Crystal multithreading support

Are they necessarily competing concepts though?

I see them more as complementary, with different variants of nurseries that only collect results (including exceptions), whereas supervisors would be more suited for complex scenarios having support for limits on simultaneous execution, restarts and whatnot. So supervisors would be the choice for building the main loop of a web server, whereas a nursery would be the main choice during the actual execution of an incoming request (perhaps the program want to fetch a bunch of resources concurrently?).

What are your thoughts? It is very much possible that I miss some aspect of supervisors as I haven’t actually used erlang/elixir for anything.

@yxhuvud I see nurseries as abstract structures to group fibers while OTP is about starting/monitoring explicit actors, which is more concrete. I guess they may not be competing and supervisors might be buildable on top of nurseries, yes.

1 Like

@dsisnero sigh, don’t push even more papers for me to read:
https://people.cs.uchicago.edu/~jhr/papers/cml.html

3 Likes

One of the maintainers of racket compared go concurrency and concurrent ml - is go an acceptable cml? — wingolog - Also, F# uses the same concurrency model as CML in their hopac library is go an acceptable cml? — wingolog.

1 Like

Good news is, up to the end of 2024, the topic of this post has made some progress.

1 Like

Hi all! What is the status for multithreading support today? Can we talk about adding ractors ruby equivalent to crystal?

1 Like

We already have it, check the RFC for details.

1 Like

It’s almost ready. We’re polishing the last few details. See

7 Likes

@zw963 @ysbaddaden thank you for answer, it looks very good!

But what about this Thread class, can I use it in code? No examples about usage this, is this class a public?

Thread is the Native threads in the OS,you can always use it, but be careful, with lock to avoid data race.

Fiber is cooperative.

1 Like

The main problem isn’t the synchronizing, it is that you don’t have access to anything related to the event loop. Which means that anything in stdlib related to IO or Channels are not available

1 Like

But, can I start event loop in thread manually?

I’m surprised Thread is documented and I think that’s unintentional. It’s been explicitly kept out of the docs in the past and it still has the :nodoc: magic comment to avoid documenting it, but it made its way into the docs somehow.

To be clear, I wouldn’t use Thread directly. In addition to what others have said, I seem to remember garbage collection is also an issue on threads not allocated by the Crystal scheduler. I can’t remember what it was specifically, but I remember a member of the core team mentioning it … somewhere.

5 Likes

Ruby has Thread.new and Ractor.new, but in Crystal you use spawn do ... end for concurrency. spawn creates a new Fiber. The Thread class represents a low-level OS thread, and there’s no need for users to touch it directly.

Here’s a typical producer-consumer pattern (written with Claude’s coding assistance):

require "wait_group"

# Compile with:
#   crystal run example.cr -Dpreview_mt -Dexecution_context

WORKERS         =     32
MAX_PARALLELISM =      8
JOB_COUNT       =  1_024
EXPECTED_SUM    = 523_776 # (0 + 1023) * 1024 / 2

consumers = Fiber::ExecutionContext::Parallel.new("consumers", MAX_PARALLELISM)
jobs      = Channel(Int32).new(64)
partials  = Channel(Int32).new(WORKERS)
wg        = WaitGroup.new(WORKERS)

# Parallel consumers: each worker accumulates a local sum.
WORKERS.times do
  consumers.spawn do
    local_sum = 0
    while value = jobs.receive?
      local_sum += value
    end
  ensure
    partials.send(local_sum || 0) # Send one partial result per worker.
    wg.done
  end
end

# Producer: send 0..1023 to jobs, then close to signal workers to stop.
JOB_COUNT.times { |i| jobs.send(i) }
jobs.close

wg.wait

# Final reduce: aggregate partial sums from all workers.
total = WORKERS.times.sum { partials.receive }

puts "total:    #{total}"
puts "expected: #{EXPECTED_SUM}"
puts "ok?:      #{total == EXPECTED_SUM}"
2 Likes

This example is a example of Claude plagiarizing from official documentation.

2 Likes

FYI, this is erroring locally and in Carcin with the following message: “Showing last frame. Use --error-trace for full trace.

error in line 3
Error: undefined constant Fiber::ExecutionContext::Parallel"

Execution contexts are an experimental feature and require opt-in via compiler flag.
See Fiber::ExecutionContext - Crystal 1.19.1

Carcin doesn’t allow setting compiler flags, so this is unfortunately not available there.

We expect execution contexts to become the default and require no configuration in the next release.

5 Likes