Modern
Concurrency
Practises
in Ruby
@arnab_deka
Agenda
"I would remove the thread and add actors or some other more advanced concurrency features"
- Matz
Agenda
Concurrency & Parallelism
Threads & Locks (briefly)
Atoms/Channels/Futures/Actors/STM...
Thoughts...
"The obligatory opening joke"
Threads, Locks, Mutexes
and blech...
Simple beginning...
class Counter
attr_reader :count
def initialize
@count = 0
end
def inc
@count += 1
end
end
counter = Counter.new
t1 = Thread.new { 1000.times { counter.inc } }
t2 = Thread.new { 1000.times { counter.inc } }
t1.join; t2.join
puts "Final count: #{counter.count}"
$ ruby -v
ruby 2.1.0p0
$ ruby ./01_simple.rb
Final count: 2000
$ ruby -v
jruby 1.7.9
$ ruby ./01_simple.rb
Final count: 1639
$ ruby -v
rubinius 2.2.1
$ ruby ./01_simple.rb
Final count: 1897
So... Mutex
semaphor = Mutex.new
counter = Counter.new
t1 = Thread.new do
1000.times do
semaphor.synchronize { counter.inc }
end
end
t2 = ...
t1.join; t2.join
puts "Final count, using a mutex: #{counter.count}"
Mutexes everywhere...
[Hash, Array].each do |klass|
klass.superclass.instance_methods(false).each do |method|
klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def #{method}(*args)
@_monitor.synchronize { super }
end
RUBY_EVAL
end
end
But how do you...
Test?
Debug?
But how do you...
Test?
Debug?
Reproduce issues?
And what about...
Race Conditions?
Deadlocks??!!
Atoms
Atomic
-
Integer
-
Boolean
-
Reference
-
...
-
Array
CAS
Compare and Swap
Lock-free
Non-blocking
require 'atomic'
class Counter
def initialize
@count = Atomic.new(0)
end
def inc
@count.update { |num| num + 1 }
end
def count
@count.value
end
end
counter = Counter.new
t1 = Thread.new { 1000.times { counter.inc } }
t2 = Thread.new { 1000.times { counter.inc } }
t1.join; t2.join
puts "Final count, using atomic: #{counter.count}"
MRI/JRuby/rbx: 2000
in Clojure
(def num (atom 0))
@num
(swap! num inc)
(reset! num 0)
Validators
(def positive-num (atom 0 :validator #(>= % 0)))
Watchers
(add-watch a :print #(println "from " %3 " to " %4))
Retries and side-effects
Weird Fact #1
Wombat poop is cube shaped...
Futures
in Clojure
(let [a (future (+ 1 2))
b (future (+ 3 4))]
(+ @a @b))
-
async
-
separate thread
-
blocks when realized
-
check if ready
require 'celluloid'
future = Celluloid::Future.new { sleep 3; 200 + 300 }
while ! future.ready?
puts "wait..."
sleep 1
end
puts future.value
$ ruby ./04_futures.rb
wait...
wait...
wait...
wait...
500
class CoolCommand
def cool_beans(a, b)
:cool_beans
end
end
cmd = CoolCommand.new
cmd.cool_beans(200, 300)
class CoolCommand
include Celluloid
def cool_beans(a, b)
:cool_beans
end
end
pool = CoolCommand.pool
future = pool.future(:cool_beans, 200, 300)
puts future.value
More Futures
And Promises
-
lazy (
futures are eager)
-
spec
-
Gems
Actors
Actors in Elixir/Erlang
defmodule Player do
def loop(name) do
receive do
{:serve} ->
# do it ...
end
end
end
p1 = spawn_link(Player, :loop, ["Federer"])
send(p1, {:serve})
Process.register(p1, :federer)
send(:federer, {:serve})
More on Erlang...
- processes are light-weight
- fault-tolerance
- "let it crash": simpler code
- OTP
- distributed
class Player
include Celluloid
def initialize(name); end
def serve; end
end
federer = Player.new
federer.async.serve
Player.supervise_as(:federer, "Federer")
Celluloid::Actor[:federer].async
Agents
in Clojure
(def mr-smith (agent 100))
@mr-smith
(send mr-smith #((Thread/sleep 3000) (fight %)))
validators and watchers
(def negative-mr-smith (agent 0 :
validator #(<= % 0)))
errors
(agent-error mr-smith)
in Ruby
Refs and STM
in Clojure
(defn transfer [from to amount]
(dosync
(swap! transfer-count inc)
(alter from - amount)
(alter to + amount)))
(def alice (ref 1000 :validator #(>= % 0)))
(def bob (ref 2000 :validator #(>= % 0)))
(def transfer-count (atom 0))
in Clojure
(repeatedly 25 #(transfer alice bob 100))
;;; IllegalStateException Invalid reference state clojure.lang.ARef.validate (ARef.java:33)
@alice ;;; 0
@bob ;;; 3000
@transfer-count ;;; 11
in Clojure
Atomic
in Clojure
Atomic
Consistent
in Clojure
Atomic
Consistent
Isolated
in Clojure
Atomic
Consistent
Isolated
Durable?
in Clojure
Synchronous
Retries
in Ruby
in Ruby
Alternatives:
Weird Fact #2
Bluey is a common nickname
for Redheads in Australia.
Coroutines, Channels, CSP
in Clojure
(def c (chan))
(thread (println "Pouring: " (<!! c) " from channel"))
;;; returns immediately
(>!! c "Hello RubyConf AU")
Buffering, Sliding
& Dropping
(def bc (chan 20))
(>!! bc "Hello")
(>!! bc "RubyConf AU")
(<!! bc) ;;; "Hello"
(<!! bc) ;;; "RubyConf AU"
(def c (chan (dropping-buffer 20)))
(def c (chan (sliding-buffer 20)))
... and Channel-level functions:
onto-chan, map-chan etc.
Go Blocks
(def c (chan))
(go
(let [x (<! c)
y (<! c)]
(println (clojure.string/join " " [x y]))))
(>!! c "Hello")
(>!! c "RubyConf AU")
... evented, but looking like so.
... and real efficient.
require "agent"
c = Agent::Channel.new(String, name: 'greetings')
go!(c) do |c|
x = c.receive
y = c.receive
[x,y].join
end
c << "Hello"
c << "RubyConf AU"
Thoughts
Learn about all available options
Thoughts
Learn about all available options
Experiment with all options
Thoughts
Learn about all available options
Experiment with all options
Consider simple multi-process queues
in Ruby
Thoughts
Learn about all available options
Experiment with all options
Consider simple multi-process queues
in Ruby
Don't be afraid to mix