Concurrency in Crystal

A primer

Lorenzo Barasti

  1. Communicating sequential processes
  2. Crystal's execution model
  3. Capacity of a channel
  4. 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

 

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