Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Concurrent Programming with Celluloid (MWRC 2012)

4131d2f57a0db2a2b4d9a62bd389fd44?s=47 tarcieri
March 16, 2012

Concurrent Programming with Celluloid (MWRC 2012)

Threads versus events: which should you choose? How about both? In this talk you’ll learn about the Celluloid concurrency framework, which combines OOP and the Actor Model to give you concurrent Ruby objects. You’ll also learn about how Celluloid lets you combine blocking I/O and asynchronous evented I/O, offering you all the benefits of EventMachine without the restrictions of a single event loop. The talk will also provide a brief introduction to DCell, a distributed extension to Celluloid.

4131d2f57a0db2a2b4d9a62bd389fd44?s=128

tarcieri

March 16, 2012
Tweet

Transcript

  1. Tony Arcieri MountainWest RubyConf March 15th, 2012 Concurrent programming with

    http://github.com/celluloid
  2. About Me

  3. About Me libev binding for Ruby (1 year before Node)

  4. About Me Actors +“Fibered” I/O (2 years before em-synchrony)

  5. About Me Ruby-Flavored Erlang (2 years before Elixir)

  6. http://elixir-lang.org/

  7. About Me Didn’t talk about Revactor or Reia at MWRC

    2008
  8. What next?

  9. None
  10. None
  11. If I can’t drag Erlang halfway to Ruby...

  12. Perhaps I can get Ruby halfway to Erlang...

  13. Rubyists don’t like threads I want to change that

  14. x86 CPU Trends (Not Entirely to Scale)

  15. Multicore is the future

  16. Threads are important

  17. No GIL! Thread-level parallelism Rubinius Threads = Multicore

  18. Parallel Blocking I/O But only one Ruby thread at a

    time :( YARV Threads != Multicore
  19. When everyone has 100 core CPUs...

  20. will we run 100 virtual machines?

  21. or one?

  22. Sidekiq What if 1 Sidekiq process could do the work

    of 20 Resque processes? http://mperham.github.com/sidekiq/
  23. A little about Revactor...

  24. Revactor predates: •Neverblock •Dramatis •Rack::FiberPool •em-synchrony

  25. Inspired by: •Erlang •Omnibus Concurrency (MenTaLguY) •Kamaelia (Python) •Eventlet (Python)

  26. Single-Threaded

  27. Bad API

  28. listener = Actor::TCP.listen(HOST, PORT, :filter => :line) puts "Listening on

    #{HOST}:#{PORT}" # The main loop handles incoming connections loop do # Spawn a new actor for each incoming connection Actor.spawn(listener.accept) do |sock| puts "#{sock.remote_addr}:#{sock.remote_port} connected" # Connection handshaking begin sock.write "Please enter a nickname:" nickname = sock.read server << T[:register, Actor.current, nickname] # Flip the socket into asynchronous "active" mode # This means the Actor can receive messages from # the socket alongside other events. sock.controller = Actor.current sock.active = :once # Main message loop loop do Actor.receive do |filter| filter.when(T[:tcp, sock]) do |_, _, message| server << T[:say, Actor.current, message] sock.active = :once end
  29. WAT

  30. None
  31. listener = Actor::TCP.listen(HOST, PORT, :filter => :line) puts "Listening on

    #{HOST}:#{PORT}" # The main loop handles incoming connections loop do # Spawn a new actor for each incoming connection Actor.spawn(listener.accept) do |sock| puts "#{sock.remote_addr}:#{sock.remote_port} connected" # Connection handshaking begin sock.write "Please enter a nickname:" nickname = sock.read server << T[:register, Actor.current, nickname] # Flip the socket into asynchronous "active" mode # This means the Actor can receive messages from # the socket alongside other events. sock.controller = Actor.current sock.active = :once # Main message loop loop do Actor.receive do |filter| filter.when(T[:tcp, sock]) do |_, _, message| server << T[:say, Actor.current, message] sock.active = :once end
  32. Procedural!

  33. Ugly!

  34. WTF is T? filter.when(T[:tcp, sock]) do |_, _, message| server

    << T[:say, Actor.current, message] sock.active = :once end
  35. Less Erlang

  36. More Objects

  37. Can We Do Better?

  38. YES

  39. What is Celluloid?

  40. Celluloid is a general purpose concurrency framework for Ruby

  41. A Contrived Example

  42. require 'thread' class ConcurrentNestedHash def initialize @outer = {} @mutex

    = Mutex.new end def [](*keys) @mutex.synchronize { keys.inject(@outer) { |h,k| h[k] } } end def []=(*args) @mutex.synchronize do value = args.pop raise ArgumentError, "wrong number of arguments (1 for 2)" if args.empty? key = args.pop hash = args.inject(@outer) { |h,k| h[k] ||= {} } hash[key] = value end end def inspect; @mutex.synchronize { super }; end end
  43. >> h = ConcurrentNestedHash.new => #<ConcurrentNestedHash:0x007f99ed735f08 @outer={}, @mutex=#<Mutex: 0x007f99ed735e90>> >>

    h[:foo, :bar, :baz] = 42 => 42 >> h => #<ConcurrentNestedHash:0x007f99ed735f08 @outer={:foo=>{:bar=>{:baz=>42}}}, @mutex=#<Mutex:0x007f99ed735e90>> >> h[:foo, :bar, :baz] => 42
  44. -require 'thread' +require 'celluloid' class ConcurrentNestedHash + include Celluloid def

    initialize @outer = {} - @mutex = Mutex.new end def [](*keys) - @mutex.synchronize { keys.inject(@outer) { |h,k| h[k] } } + keys.inject(@outer) { |h,k| h[k] } end def []=(*args) - @mutex.synchronize do value = args.pop raise ArgumentError, "wrong number of arguments (1 for 2)" if args.empty? key = args.pop hash = args.inject(@outer) { |h,k| h[k] ||= {} } hash[key] = value - end end - - def inspect; @mutex.synchronize { super }; end end
  45. require 'celluloid' class ConcurrentNestedHash include Celluloid def initialize @outer =

    {} end def [](*keys) keys.inject(@outer) { |h,k| h[k] } end def []=(*args) value = args.pop raise ArgumentError, "wrong number of arguments (1 for 2)" if args.empty? key = args.pop hash = args.inject(@outer) { |h,k| h[k] ||= {} } hash[key] = value end end
  46. How?

  47. MAGIC

  48. Automatic locking?

  49. None
  50. None
  51. Locks are hard •Dining Philosophers Problem •Sleeping Barber Problem •Cigarette

    Smokers Problem
  52. OOP + Concurrency

  53. “I thought of objects being like biological cells and/or individual

    computers on a network, only able to communicate with messages” - Alan Kay, creator of Smalltalk, on the meaning of "object oriented programming"
  54. OOP Tools Classes Inheritance Messages

  55. Concurrency Tools Threads Locks Queues

  56. OOP Tools Classes Inheritance Messages Concurrency Tools Threads Locks Queues

    +
  57. =

  58. Concurrent Objects &DOOHU &HOOXORLG $FWRU3UR[\ &$// 5HFHLYHU &HOOXORLG 0DLOER[ 5(63216(

    &HOOXORLG 0DLOER[ &HOOXORLG$FWRU &HOOXORLG&DOO &HOOXORLG5HVSRQVH
  59. Communicating Sequential Processes •Do One Thing At a Time •Communicate

    With Messages •Encapsulate Local State
  60. Concurrent Objects do more...

  61. Generalized Deadlock-free Synchronization

  62. None
  63. None
  64. None
  65. None
  66. None
  67. Pythons did it!

  68. None
  69. 1997

  70. None
  71. 1999

  72. None
  73. Web vs Objects (Not to Scale)

  74. None
  75. How does Celluloid work?

  76. >> h = ConcurrentNestedHash.new => #<Celluloid::Actor(ConcurrentNestedHash:0x3ff3b952df7c) @outer={}>

  77. >> h = ConcurrentNestedHash.new => #<Celluloid::Actor(ConcurrentNestedHash:0x3ff3b952df7c) @outer={}>

  78. # Class methods added to classes which include Celluloid module

    ClassMethods # Create a new actor def new(*args, &block) proxy = Actor.new(allocate).proxy proxy._send_(:initialize, *args, &block) proxy end ...
  79. Synchronous Calls

  80. EXTREME Late Binding &DOOHU &HOOXORLG $FWRU3UR[\ &$// 5HFHLYHU &HOOXORLG 0DLOER[

    5(63216( &HOOXORLG 0DLOER[ &HOOXORLG$FWRU &HOOXORLG&DOO &HOOXORLG5HVSRQVH
  81. >> h = ConcurrentNestedHash.new => #<Celluloid::Actor(ConcurrentNestedHash:0x3ff3b952df7c) @outer={}> >> h.inspect =>

    “#<Celluloid::Actor(ConcurrentNestedHash:0x3ff3b952df7c) @outer={}>” Synchronous Calls
  82. Asynchronous Calls

  83. Asynchronous Calls &DOOHU &HOOXORLG $FWRU3UR[\ &$// 5HFHLYHU &HOOXORLG 0DLOER[ &HOOXORLG$FWRU

    &HOOXORLG&DOO
  84. Asynchronous Calls >> h = ConcurrentNestedHash.new => #<Celluloid::Actor(ConcurrentNestedHash:0x3ff3b952df7c) @outer={}> >>

    h.send!(:[]=, :foo, :bar, :baz, 42) => nil >> h[:foo, :bar, :baz] => 42
  85. >> h = ConcurrentNestedHash.new => #<Celluloid::Actor(ConcurrentNestedHash:0x3ff3b952df7c) @outer={}> >> h.send!(:[]=, :foo,

    :bar, :baz, 42) => nil >> h[:foo, :bar, :baz] => 42 Asynchronous Calls
  86. Asynchronous Calls >> h.inspect! => nil

  87. Asynchronous Calls Kind of like “next tick”

  88. Asynchronous Calls How do I get the value returned?

  89. Futures

  90. Futures &DOOHU &HOOXORLG $FWRU3UR[\ 5HFHLYHU &HOOXORLG 0DLOER[ &HOOXORLG$FWRU &HOOXORLG&DOO &$//

    &HOOXORLG 0DLOER[ )8785( &HOOXORLG )XWXUH &HOOXORLG5HVSRQVH 9$/8(" 9$/8(
  91. >> h = ConcurrentNestedHash.new => #<Celluloid::Actor(ConcurrentNestedHash:0x3ff3b952df7c) @outer={}> >> future =

    h.future :inspect => #<Celluloid::Future:0x3ff3b953821a> >> 41 + 1 # roflscale computation => 42 >> future.value => “#<Celluloid::Actor(ConcurrentNestedHash:0x3ff3b952df7c) @outer={}>” Futures
  92. >> h = ConcurrentNestedHash.new => #<Celluloid::Actor(ConcurrentNestedHash:0x3ff3b952df7c) @outer={}> >> future =

    h.future :inspect => #<Celluloid::Future:0x3ff3b953821a> >> 41 + 1 # roflscale computation => 42 >> future.value => “#<Celluloid::Actor(ConcurrentNestedHash:0x3ff3b952df7c) @outer={}>” Futures
  93. Basic Ingredients •Regular method calls •Async calls (“next tick”) •Futures

  94. Secret Sauce

  95. How do we prevent deadlocks?

  96. None
  97. None
  98. None
  99. None
  100. None
  101. Why do deadlocks happen?

  102. Waiting for something that never happens...

  103. ...instead of what’s important

  104. None
  105. How can we wait on everything at once?

  106. FIBERS!

  107. WAT?

  108. None
  109. SUBLIMINAL MESSAGE: DONALD KNUTH IS YODA

  110. No really...

  111. Communicating Sequential Processes Do one thing at a time

  112. Fibers Cheap suspendable/resumable execution context

  113. Fibers Cheap suspendable/resumable execution context Communicating Sequential Processes Do one

    thing at a time +
  114. Internal Concurrency for Actors

  115. Don’t Block, Suspend to the Scheduler

  116. Example!

  117. require 'celluloid' class Person include Celluloid def name self.class.to_s end

    def greet(interested_party) "Hello #{interested_party.name}, I'm #{name}" end end class Joe < Person; end class Mike < Person def greet(other) super << "\n" << other.greet(current_actor) end end mike = Mike.new joe = Joe.new puts mike.greet(joe)
  118. Hello Joe, I'm Mike Hello Mike, I'm Joe Output

  119. None
  120. But it is a cool story!

  121. Circular Call Graph 0LNH -RH JUHHW QDPH

  122. Communicating Sequential Processes Do one thing at a time

  123. DEADLOCK!

  124. require 'celluloid' class Person include Celluloid def name self.class.to_s end

    def greet(interested_party) "Hello #{interested_party.name}, I'm #{name}" end end class Joe < Person; end class Mike < Person def greet(other) super << "\n" << other.greet(current_actor) end end mike = Mike.new joe = Joe.new puts mike.greet(joe)
  125. 0LNH -RH 0LNHJUHHW -RHJUHHW 0LNHQDPH  J U H H

    W  + H OO R  0 L N H    J U H H W  0 L N H  6XVSHQGHG Multitasking Fibers
  126. Waiting tasks suspend themselves so ready tasks can run

  127. Every method call creates a Fiber

  128. None
  129. Slow?

  130. Call Benchmark Core i7 2.0GHz (OS X 10.7.3) Rubinius YARV

    16815/s (60µs) - JRuby 1.6.7 20899/s (50µs) - JRuby HEAD 15260/s (65µs) - rbx HEAD 9367/s (107µs) - Ruby 1.9.3
  131. None
  132. What if an actor crashes?

  133. None
  134. None
  135. Fault Tolerance •Supervisors & Supervision Trees •“Fail early”, restart in

    a clean state •Do what Erlang does •No seriously, do what Erlang does
  136. Evented I/O for Celluloid http://github.com/celluloid/celluloid-io

  137. Now that we have threads licked... how about I/O?

  138. USE BLOCKING I/O

  139. Blocking IO is OK!* No central event loop to block

  140. *But be careful Locks in external services = Deadlocks in

    Celluloid
  141. But how will I serve my roflmillions of users?

  142. Evented IO •Large numbers of connections (>1000) •Mostly idle connections

    •Mostly IO-bound problems •Websockets are an ideal case
  143. Actors are event loops

  144. &HOOXORLG $FWRU &HOOXORLG 0DLOER[ &RQGLWLRQ 9DULDEOH Normal Actors

  145. Celluloid::IO Actors &HOOXORLG $FWRU &HOOXORLG ,20DLOER[ &HOOXORLG ,25HDFWRU

  146. nio4r-powered reactor

  147. nio4r •Quasi-inspired by Java NIO •Smallest API possible •libev C

    extension for CRuby/rbx •Java extension for JRuby •Pure Ruby version too! http://github.com/tarcieri/nio4r/
  148. Celluloid::IO::TCPSocket •Uses fibered I/O •“Duck-type” of ::TCPSocket •Evented inside Celluloid::IO

    actors •Blocking IO elsewhere (Ruby Threads, normal Celluloid actors)
  149. Evented IO AND Threaded IO You don’t have to choose!

  150. Transparent handles you can pass around Kind of like file

    descriptors!
  151. Other replacement classes •Celluloid::IO::TCPServer •Celluloid::IO::UDPSocket •No Celluloid::IO::UnixSocket yet, sorry :(

  152. Echo Server Example

  153. class EchoServer include Celluloid::IO def initialize(host, port) puts "*** Starting

    echo server on #{host}:#{port}" # Since we included Celluloid::IO, we're actually making a # Celluloid::IO::TCPServer here @server = TCPServer.new(host, port) run! end def finalize @server.close if @server end def run loop { handle_connection! @server.accept } end def handle_connection(socket) _, port, host = socket.peeraddr puts "*** Received connection from #{host}:#{port}" loop { socket.write socket.readpartial(4096) } rescue EOFError puts "*** #{host}:#{port} disconnected" socket.close end end supervisor = EchoServer.supervise("127.0.0.1", 1234) trap("INT") { supervisor.terminate; exit } sleep
  154. 7&36RFNHWEDVHG/LEUDULHV (YHQW0DFKLQH/LEUDULHV $6<1&$//7+(7+,1*6 ),%(5,=($//7+(7+,1*6 AAAHPV\QFKURQ\FRPSDWLEOH OLEUDULHV Running out of gas

    after slide #150
  155. ^^^ LET’S USE THIS 7&36RFNHWEDVHG/LEUDULHV

  156. Dependency Injection

  157. MyClient.new(‘myhost.domain’, 1234, :socket => Celluloid::IO::TCPSocket)

  158. Easy Peasy!

  159. Celluloid::IO-powered web server http://github.com/celluloid/reel

  160. Hello World Benchmark # httperf --num-conns=50 --num-calls=1000 Ruby Version Throughput

    Latency ------------ ---------- ------- JRuby HEAD 5650 reqs/s (0.2 ms/req) Ruby 1.9.3 5263 reqs/s (0.2 ms/req) JRuby 1.6.7 4303 reqs/s (0.2 ms/req) rbx HEAD 2288 reqs/s (0.4 ms/req)
  161. Hello World Comparison Web Server Throughput Latency ---------- ---------- -------

    Goliath (0.9.4) 2058 reqs/s (0.5 ms/req) Thin (1.2.11) 7502 reqs/s (0.1 ms/req) Node.js (0.6.5) 11735 reqs/s (0.1 ms/req)
  162. 0MQ TOO! &HOOXORLG $FWRU &HOOXORLG ,20DLOER[ &HOOXORLG =04 5HDFWRU

  163. Celluloid::ZMQ •Built on ffi-rzmq •But exposes a higher level API

    •Celluloid::IO for ZeroMQ
  164. Distributed Celluloid over 0MQ http://github.com/celluloid/reel

  165. Mostly ready to use!

  166. Probably needs its own talk :(

  167. Celluloid-powered Web Framework http://github.com/celluloid/lattice

  168. Vaporware!

  169. Goals •Reuse parts of Rails •Multithreaded development mode •Easy scatter/gather

    for SOA
  170. That’s all, folks!

  171. Bye!

  172. Links •http://github.com/celluloid •http://twitter.com/bascule •http://unlimitednovelty.com/