1. Introduction - Spawn Fibers

By default when crystal code launches a main thread is created for the running code, a separate thread is created for the garbage collector, and an Event Loop to handle IO waits - allowing the main thread to continue while waiting for slow IO calls to finish.

Concurrency happens by spawning fibers (light-weight sub-threads within the main thread). Fibers are scheduled to run within the main thread on it’s CPU. Concurrency is a form of multi-tasking - quickly switching between tasks on the main thead

Parallelism - with the appropriate compiler switches - allows processes to happen simultaneously on separate CPUs.

New Fibers are created using spawn.

There two main way to spawn threads - we will need both: - using the block notation - using the method call notation

2. Spawn Block

A spawn block looks like:

code/crystal/src/concurrency/spawn_block.cr
puts "before spawn"
spawn do
  puts "within spawn"
end
puts "after spawn"

# Fiber.yield

Run with:

$ crystal src/concurrency/spawn_block.cr
Note
We don’t see the text within spawn

In order to get the main thread to let the Fiber run before ending we need to uncomment the Fiber.yield and run again - this tells the main thread to process the fibers on the main thread - later we will see there are other actions that trigger the main thread to yield to the Fibers.

2.1. Passing Values when Fiber EXECUTES

Spawn block - uses the value at the time when the Fiber is executed - which can lead to unexpected results. Try the following code:

code/crystal/src/concurrency/spawn_block_unexpected.cr
i = 0
puts "SYNC: i = 0 = #{i}"     # synchronous
spawn do
  puts "ASYNC: i = 0 = #{i}"  # asynchronous
end

i = 1
puts "SYNC: i = 1 = #{i}"     # synchronous
spawn do
  puts "ASYNC: i = 1 = #{i}"  # asynchronous
end

Fiber.yield  # i = 1 when the main thread yields to the fibers!

Run with:

$ crystal src/concurrency/spawn_block_unexpected.cr

2.2. Passes Values when Fiber SPAWNS

Because i = 1 at the time when the fibers execute '1' is printed twice - this is probably not desired in this case. The solution is to use Proc

The solution is to define a proc which will pass the value at the time the Proc is called NOT when the Fiber is executed.

This code works as expected, but is clumsy looking:

code/crystal/src/concurrency/spawn_proc.cr
i = 0
puts "SYNC: i = 0 = #{i}"       # synchronous
proc_0 = ->(x : Int32) do
  spawn do
    puts "ASYNC: i = 0 = #{x}"  # asynchronous
  end
end
proc_0.call(i)

i = 1
puts "SYNC: i = 1 = #{i}"       # synchronous
proc_1 = ->(x : Int32) do
  spawn do
    puts "ASYNC: i = 1 = #{x}"  # asynchronous
  end
end
proc_1.call(i)

Fiber.yield

Run with:

$ crystal src/concurrency/spawn_proc_block.cr
Note
now all works well - but lots of code for 2 simple puts statements.

3. Spawn Method (Passing Values when Fiber Spawn)

A much nicer way to pass the value at the time of call — not at the time of execution is to use the spawn call:

code/crystal/src/concurrency/spawn_call.cr
i = 0
puts "SYNC: i = 0 = #{i}"         # synchronous
spawn puts "ASYNC: i = 0 = #{i}"  # asynchronous

i = 1
puts "SYNC: i = 1 = #{i}"         # synchronous
spawn puts "ASYNC: i = 1 = #{i}"  # asynchronous

Fiber.yield

Run with:

$ crystal src/concurrency/spawn_call.cr

This is syntactic sugar (a crystal macro) for proc-spawn technique shown above. I find this syntax far more attractive and will be used for method calls from now on!

Just a reminder, notice how all async messages happen after Fiber.yield