Concurrency in Crystal
A primer
Lorenzo Barasti
- Communicating sequential processes
- Crystal's execution model
- Capacity of a channel
- Correctness of concurrent applications
Concurrency in Crystal
A primer
Communicating Sequential
Processes
CSP
A concurrent paradigm where
Processes communicate
by passing messages
over channels
CSP in Crystal
A concurrent paradigm where
Processes Fibers communicate
by passing messages
over Channels
CSP in Crystal
A concurrent paradigm where
Processes Fibers communicate
by passing messages
over channels
Fibers
Lightweight
Cooperative
Unit of execution
CSP in Crystal
A concurrent paradigm where
Processes Fibers communicate
by passing messages
over Channels
Channels
blocking #send and #receive
similar to queues
Regulate data exchange between fibers
CSP sketches
Alice writes letters
Bob puts them in envelops
Charles ships them
CSP in Crystal
ch = Channel(Msg).new
msg = Msg.new(uuid)
ch.send msg
puts ch.receive
CSP in Crystal
ch = Channel(Msg).new
msg = Msg.new(uuid)
ch.send msg
puts ch.receive
CSP in Crystal
ch = Channel(Msg).new
msg = Msg.new(uuid)
ch.send msg
puts ch.receive
CSP in Crystal
ch = Channel(Msg).new
msg = Msg.new(uuid)
ch.send msg
puts ch.receive
CSP in Crystal
X
ch = Channel(Msg).new
msg = Msg.new(uuid)
ch.send msg
puts ch.receive
CSP in Crystal
X
ch = Channel(Msg).new
msg = Msg.new(uuid)
ch.send msg
puts ch.receive
CSP sketches
Cook
Waiter
Window
Chicken dish
CSP sketches
Sending fiber
Channel
Message
Receiving fiber
CSP in Crystal
X
ch = Channel(Msg).new
msg = Msg.new(uuid)
ch.send msg
puts ch.receive
Crystal's execution model
Crystal's execution model
- Scheduler: allocates time for fibers to run
- Event loop: a fiber that coordinates async tasks
- Main: the main fiber running our code
Crystal's execution model
Examples of (potentially) blocking operations:
Fiber.yield
sleep 2
HTTP.get(url)
ch.receive
ch.send obj
Crystal's execution model
fiber A
Thread
Scheduler
Crystal's execution model
Thread
Scheduler
fiber A
fiber B
Crystal's execution model
Scheduler
Thread
fiber A
fiber B
spawn(name: "fiber A") do
spawn(name: "fiber Z") do
# do something
end
msg = ch.receive
end
fiber Z
Crystal's execution model
Scheduler
Thread
fiber A
fiber B
spawn(name: "fiber A") do
spawn(name: "fiber Z") do
# do something
end
msg = ch.receive
end
fiber Z
Crystal's execution model
Scheduler
Thread
fiber A
Thread
fiber B
spawn(name: "fiber A") do
spawn(name: "fiber Z") do
# do something
end
msg = ch.receive
end
fiber Z
Crystal's execution model
Scheduler
Thread
fiber A
Thread
fiber B
fiber Z
Crystal's execution model
Scheduler
Thread
Thread
fiber B
fiber A
fiber Z
Crystal's execution model
Scheduler
Thread
fiber A
Thread
fiber B
fiber C
fiber Z
Crystal's execution model
Scheduler
Thread
Thread
fiber C
fiber A
fiber B
spawn(name: "fiber B") do
Fiber.yield
end
fiber Z
Crystal's execution model
Scheduler
Thread
Thread
fiber C
fiber A
fiber B
fiber Z
Crystal's execution model
Scheduler
Thread
Thread
fiber C
fiber A
fiber B
fiber Z
CSP in Crystal
ch = Channel(Msg).new
msg = Msg.new(uuid)
spawn(name: "processor") do
puts ch.receive
end
ch.send msg
CSP in Crystal
ch = Channel(Msg).new
msg = Msg.new(uuid)
spawn(name: "processor") do
puts ch.receive
end
ch.send msg
CSP in Crystal
ch = Channel(Msg).new
msg = Msg.new(uuid)
spawn(name: "processor") do
puts ch.receive
end
ch.send msg
CSP in Crystal
ch = Channel(Msg).new
msg = Msg.new(uuid)
spawn(name: "processor") do
puts ch.receive
end
ch.send msg
CSP in Crystal
X
ch = Channel(Msg).new
msg = Msg.new(uuid)
spawn(name: "processor") do
puts ch.receive
end
ch.send msg
CSP in Crystal
X
ch = Channel(Msg).new
msg = Msg.new(uuid)
spawn(name: "processor") do
puts ch.receive
end
ch.send msg
CSP in Crystal
X
ch = Channel(Msg).new
msg = Msg.new(uuid)
spawn(name: "processor") do
puts ch.receive
end
ch.send msg
CSP in Crystal
ch = Channel(Msg).new
msg = Msg.new(uuid)
spawn(name: "processor") do
puts ch.receive
end
ch.send msg
CSP in Crystal
Termination
ch = Channel(Msg).new
msg = Msg.new(uuid)
spawn(name: "processor") do
puts ch.receive
end
ch.send msg
Capacity of a channel
Sending on capacity = 0
Sending on capacity = 0
Sending on capacity = 0
Sending on capacity = 0
Receiving on capacity = 0
Receiving on capacity = 0
Receiving on capacity = 0
Receiving on capacity = 0
Receiving on capacity = 0
Sending on capacity > 0
Sending on capacity > 0
Sending on capacity > 0
Receiving on capacity > 0
Receiving on capacity > 0
On channel's capacity
Fibers don't block
- when sending to a channel with capacity > 0
- when receiving from a non-empty channel
On channel's capacity
ch = Channel(Msg).new(capacity: 1)
msg = Msg.new(uuid)
ch.send msg
puts ch.receive
On channel's capacity
ch = Channel(Msg).new(capacity: 1)
msg = Msg.new(uuid)
ch.send msg
puts ch.receive
Terminates after printing
ch = Channel(Msg).new(capacity: 1024)
msg = Msg.new(uuid)
spawn(name: "processor") do
puts ch.receive
end
ch.send msg
On channel's capacity
On channel's capacity
ch = Channel(Msg).new(capacity: 1024)
msg = Msg.new(uuid)
spawn(name: "processor") do
puts ch.receive
end
ch.send msg
No output
Correctness of concurrent applications
When analysing correctness, do not base your reasoning on the order in which fibers run
Warning
Depends on runtime implementation
Do not increase channels' capacity as a way out of bugs
- Buffered channels (capacity > 0) can improve performance but cannot fix concurrency bugs
Warning
For a start, initialise all your channels to zero capacity
Suggestion
Rely on causality
- if fiber A spawns fiber B, then all the code preceding will run before B
spawn
Suggestion
Rely on causality
- if fiber A sends a message to fiber B when completing its task, then when B receives the message you can assume A's task is done
Suggestion
"The order in which fibers run should not affect the correctness of your software"
Principle
should
correctness
order
"The order in which fibers run should not affect the correctness of your software"
not affect
Principle
References
- Crystal reference: concurrency guide
- Concurrency in Go
- On unbuffered channels [Crystal forum]
- On correctness of concurrent apps [Crystal forum]
- On fibers completion [stackoverflow]
- Writing a concurrent URL checker in Crystal [web series]
Bonus content
Fibers
Channel
Message
CSP in Crystal
spawn do
# your concurrent task!
end
To start a Fiber we
CSP in Crystal
ch = Channel(Job).new
Creating a Channel
Sending & receiving
ch.send(Msg.new(uuid))
msg = ch.receive
CSP in Crystal
ch = Channel(Msg).new
2.times.each { |i|
spawn(name: "processor_#{i}") do
loop do
puts "#{Fiber.current.name}: #{ch.receive}"
end
end
}
loop do
ch.send Msg.new(UUID.random)
sleep 0.5
end
CSP in Crystal
Consumers
ch = Channel(Msg).new
2.times.each { |i|
spawn(name: "processor_#{i}") do
loop do
puts "#{Fiber.current.name}: #{ch.receive}"
end
end
}
loop do
ch.send Msg.new(UUID.random)
sleep 0.5
end
CSP in Crystal
Producer
ch = Channel(Msg).new
2.times.each { |i|
spawn(name: "processor_#{i}") do
loop do
puts "#{Fiber.current.name}: #{ch.receive}"
end
end
}
loop do
ch.send Msg.new(UUID.random)
sleep 0.5
end
CSP in Crystal
ch = Channel(Msg).new
2.times.each { |i|
spawn(name: "processor_#{i}") do
loop do
puts "#{Fiber.current.name}: #{ch.receive}"
end
end
}
loop do
ch.send Msg.new(UUID.random)
sleep 0.5
end
X
CSP in Crystal
ch = Channel(Msg).new
2.times.each { |i|
spawn(name: "processor_#{i}") do
loop do
puts "#{Fiber.current.name}: #{ch.receive}"
end
end
}
loop do
ch.send Msg.new(UUID.random)
sleep 0.5
end
X
CSP in Crystal
ch = Channel(Msg).new
2.times.each { |i|
spawn(name: "processor_#{i}") do
loop do
puts "#{Fiber.current.name}: #{ch.receive}"
end
end
}
loop do
ch.send Msg.new(UUID.random)
sleep 0.5
end
X
X
CSP in Crystal
ch = Channel(Msg).new
2.times.each { |i|
spawn(name: "processor_#{i}") do
loop do
puts "#{Fiber.current.name}: #{ch.receive}"
end
end
}
loop do
ch.send Msg.new(UUID.random)
sleep 0.5
end
X
X
CSP in Crystal
ch = Channel(Msg).new
2.times.each { |i|
spawn(name: "processor_#{i}") do
loop do
puts "#{Fiber.current.name}: #{ch.receive}"
end
end
}
loop do
ch.send Msg.new(UUID.random)
sleep 0.5
end
X
X
CSP in Crystal
ch = Channel(Msg).new
2.times.each { |i|
spawn(name: "processor_#{i}") do
loop do
puts "#{Fiber.current.name}: #{ch.receive}"
end
end
}
loop do
ch.send Msg.new(UUID.random)
sleep 0.5
end
X
X
CSP in Crystal
ch = Channel(Msg).new
2.times.each { |i|
spawn(name: "processor_#{i}") do
loop do
puts "#{Fiber.current.name}: #{ch.receive}"
end
end
}
loop do
ch.send Msg.new(UUID.random)
sleep 0.5
end
X
X
X
CSP in Crystal
ch = Channel(Msg).new
2.times.each { |i|
spawn(name: "processor_#{i}") do
loop do
puts "#{Fiber.current.name}: #{ch.receive}"
end
end
}
loop do
ch.send Msg.new(UUID.random)
sleep 0.5
end
X
X
CSP in Crystal
ch = Channel(Msg).new
2.times.each { |i|
spawn(name: "processor_#{i}") do
loop do
puts "#{Fiber.current.name}: #{ch.receive}"
end
end
}
loop do
ch.send Msg.new(UUID.random)
sleep 0.5
end
X
X
X
CSP in Crystal
ch = Channel(Msg).new
2.times.each { |i|
spawn(name: "processor_#{i}") do
loop do
puts "#{Fiber.current.name}: #{ch.receive}"
end
end
}
loop do
ch.send Msg.new(UUID.random)
sleep 0.5
end
X
X
CSP in Crystal
ch = Channel(Msg).new
2.times.each { |i|
spawn(name: "processor_#{i}") do
loop do
puts "#{Fiber.current.name}: #{ch.receive}"
end
end
}
loop do
ch.send Msg.new(UUID.random)
sleep 0.5
end
X
X
X
CSP in Crystal
ch = Channel(Msg).new
2.times.each { |i|
spawn(name: "processor_#{i}") do
loop do
puts "#{Fiber.current.name}: #{ch.receive}"
end
end
}
loop do
ch.send Msg.new(UUID.random)
sleep 0.5
end
X
X
CSP in Crystal
ch = Channel(Msg).new
2.times.each { |i|
spawn(name: "processor_#{i}") do
loop do
puts "#{Fiber.current.name}: #{ch.receive}"
end
end
}
loop do
ch.send Msg.new(UUID.random)
sleep 0.5
end
X
X
Concurrency in Crystal: a primer
By Lorenzo Barasti
Concurrency in Crystal: a primer
A primer on Crystal's concurrency model (CSP)
- 1,413