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"



Concurrency

Parallelism

 



Threads, Locks, Mutexes


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...


gem: headius/thread_safe


[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?

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




gem: headius/atomic


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

gem: headius/atomic



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

gem: celluloid


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

gem: celluloid




class CoolCommand
  def cool_beans(a, b)
    :cool_beans
  end
end

cmd = CoolCommand.new
cmd.cool_beans(200, 300)

gem: celluloid




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



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})


Full example: actors.exs

More on Erlang...


  • processes are light-weight
    • vs. JVM threads
  • fault-tolerance
    • "let it crash": simpler code
  • OTP
  • distributed

gem: celluloid

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


Full example: actors.rb



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:

  • atoms
  • HTM ?



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.

gem: agent



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"




Closing

Thoughts...

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




Thank you!


            @arnab_deka

Modern Concurrency Practises in Ruby

By Arnab Deka

Modern Concurrency Practises in Ruby

  • 6,268