of the language RubyGem (since 2013), an unopinionated toolbox Low level abstrac ons High level abstrac ons No dependencies Ruby implementa on independent CRuby, JRuby, Rubinius, and TruffleRuby Open source, MIT Over 3.4K Github stars 207 gems directly depedend on concurrent‐ruby sucker_punch, sidekiq, rails, hanami, dry‐rb 5 / 50
JRuby, TruffleRuby and Rubinius have no GIL parallelism ✓ Stdlib: Thread , Queue , Mutex , Monitor , ConditionVariable Implementa on specific: JRuby Synchronized , Java interopera on Rubinius Channel , Rubinius.lock , etc. No vola le variables fork ing memory consuming, incompa ble with JRuby Just stdlib tools are hard to use 6 / 50
gem building) concurrent ruby edge Space for new features and experiments Changes more frequently concurrent ruby ext Opt‐in C extensions for few performance improvements 7 / 50
Future , Promise , IVar , Event , dataflow , Delay , and (par ally) TimerTask Started in edge, now merged into 1.1 depreca ng old classes Familiar names based on JS promises 10 / 50
ruby Provides vola le variables with atomic CAS opera ons It's non‐blocking and lock‐free With the excep on of obviously blocking opera ons like #wait , #value Integrates with other concurrency abstrac ons: Actor , Channel , ProcessingActor 11 / 50
pending> Thread.new(foo_result) do foo_result.fulfill do_long_calculation_foo 1 end second_thread = Thread.new(foo_result) do do_on_foo_dependent_calculation foo_result.value end final_result = second_thread.value # 3 foo_result # <#Concurrent Promises ResolvableFuture:0 7feabe16a908 fulfilled> foo_result.value # 2 But we want to get away from using Threads 19 / 50
future? A naive and BAD way: Concurrent Promises.future do Concurrent Promises.future { 1+1 }.value! # blocking end.value! Use #flat which does not block a Thread ! Concurrent Promises.future do Concurrent Promises.future { 1+1 } end.flat.value! # 2 26 / 50
answer_to_everything = Concurrent Promises. delay { do_expensive_compute 41 } # <#Concurrent Promises Future:0 7feabe417138 pending> # value initiates the execution of answer_to_everything if I_WANT_ANSWERS answer_to_everything.value end # 42 Concurrent Promises.future starts execu ng immediately Can be inserted in a chain as well Concurrent Promises.future { do_foo 0 }.then(&:succ).delay.then(&:succ) # <#Concurrent Promises Future:0 7feabe4061a8 pending> 27 / 50
<#Concurrent Promises Future:0 7feabe3ecd20 pending> scheduled.resolved? # false Value will become available in the scheduled me. scheduled.value # 2 Can be inserted in a chain as well Time can be used as well future = Concurrent Promises.future { :result }. schedule(Time.now + 0.01).then(&:to_s).value! # "result" 28 / 50
canceled source, token = Concurrent Cancellation.create tasks = 4.times.map do Concurrent Promises.future(source, token) do |source, token| 1000.times do |i| break :cancelled if token.canceled? source.cancel and raise "random error at i}" if rand > 0.99 do_stuff end end end Concurrent Promises.zip( tasks).result # [false, # [:cancelled, :cancelled, :cancelled, nil], # [nil, nil, nil, #<RuntimeError: random error at 0>]] 30 / 50
No stack Needs state machine class Adder < Concurrent Actor RestartingContext def initialize(init) @count = init end def on_message(message) case message when :add @count += 1 else pass # pass to ErrorsOnUnknownMessage behaviour, which will just fail end end end 32 / 50
DB connec ons DB = Concurrent Actor Utils Pool.spawn!('db', size = 2) do |index| # DB connection constructor Concurrent Actor Utils AdHoc.spawn!("connection index}") do message { data[message] } # query a DB end end concurrent_jobs = 4.times.map do |index| # limited concurrency to 2 for asking the DB DB.ask(index).then(&:size) end Concurrent Promises.zip( concurrent_jobs).value! # [0, 1, 2, 3] 33 / 50
What if we let it con nue fla ng as long it returns a Future ? #run Does not require thread per process, or fibers (not portable) def count(value) if value < 5 # continue executing the process Concurrent Promises.future(value + 1, &method(:count)) else value # final result end end Concurrent Promises.future(0, &method(:count)).run.value! # 5 35 / 50
the receiver is able to process The Receiver has to signal back to slow down the producer The Channel and receiver could also just be an actor Depends on what you need 36 / 50
< 10 channel.push(i + 1). # fulfills only when there is space then(channel, &produce) else channel.push(nil) end end receive = sum, channel do channel.pop.then(sum) do |value, sum| value ? Concurrent Promises.future(value + sum, channel, &receive) : sum end end [Concurrent Promises.future(0, channel2, &produce).run, Concurrent Promises.future(0, channel2, &receive).run].map(&:value!) # [nil, 55] 37 / 50
simula on Uses channels as mailboxes, therefore supports backpressure We can now port Erlang's OTP to Ruby actor = Concurrent ProcessingActor.act do |actor| actor.receive.then { |v| v 3 } end # <#Concurrent ProcessingActor:0 7feabeeb3768 termination:pending> actor.tell 3 # <#Concurrent Promises Future:0 7feabeea84a8 pending> actor.termination.value! # 27 39 / 50
with the sum add_2_messages = Concurrent ProcessingActor.act do |actor| actor.receive.then do |m1| actor.receive.then(m1) do |a, b| a + b end end end # <#Concurrent ProcessingActor:0 7feabee81268 termination:pending> add_2_messages.tell 1 # <#Concurrent Promises Future:0 7feabee78618 pending> add_2_messages.termination.resolved? # false add_2_messages.tell 3 # <#Concurrent Promises Future:0 7feabee71a70 pending> add_2_messages.termination.value! # 4 41 / 50
|command, number| case command when :add do_stuff # delay counter.call actor, count + number when :done count end end end actor = Concurrent ProcessingActor.act_listening(channel2, 0, &counter) produce = actor, i do i < 10 ? actor.tell([:add, i]).then(i + 1, &produce) : actor.tell(:done) end Concurrent Promises.future(actor, 0, &produce).run actor.termination.value! # 45 43 / 50
context switching Cannot overflow No deadlocks Fixed number of threads .future_on(:fast) { 1 } IO executor Blocking jobs allowed More context switching Can overflow Concurrency level has to be managed Deadlocks ( ny probability) Threat count grows when all threads are busy ThreadPools ‐ bonus Thread.new and context switching is expensive Share threads by using pools 45 / 50
number of threads Lock‐ree, faster Supports backpressure Integra on between different abstrac ons Implementa on independent You can start with MRI and scale on JRuby 47 / 50