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

Concurrent Programming with Celluloid: JRubyConf

Concurrent Programming with Celluloid: JRubyConf

An introduction to Celluloid, a concurrent programming framework for Ruby based on the Actor Model. This is a version of the talk I gave at MWRC, but tailored for JRuby users, and with a much expanded intro to DCell.

4131d2f57a0db2a2b4d9a62bd389fd44?s=128

tarcieri

May 23, 2012
Tweet

Transcript

  1. Tony Arcieri JRubyConf May 23rd, 2012 Concurrent programming with http://celluloid.io

  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. What next?

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

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

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

  13. x86 CPU Trends (Not Entirely to Scale)

  14. AMD Bulldozer 1 CPU = 16 Cores

  15. Multicore is the future

  16. Threads are important

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

  18. When everyone has 100 core CPUs...

  19. will we run 100 virtual machines?

  20. or one?

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

    of 20 Resque processes? http://mperham.github.com/sidekiq/
  22. What is Celluloid?

  23. Celluloid is a general purpose concurrency framework for Ruby built

    on the Actor Model
  24. “Akka for Ruby”

  25. ...but not quite •Designed from the ground up around Ruby

    and OOP •Conceptually harmonious and simple! •More advanced object model •Not quite as mature :|
  26. Less Boilerplate package typedactordemo import akka.actor._ import akka.dispatch.Future import akka.pattern.ask

    import akka.util.Timeout import akka.util.duration._ case class Request(payload: String) case class Response(payload: String) trait Service { def request(r: Request): Future[Response] } class ServiceImpl extends Service { val actor = { val ctx = TypedActor.context ctx.actorOf(Props[ServiceActor]) } implicit val timeout = Timeout(10 seconds) def request(req: Request): Future[Response] = (actor ? req).mapTo[Response] } class ServiceActor extends Actor { def receive = { case Request(payload) => sender ! Response(payload) } } object Main extends App { val system = ActorSystem("TypedActorDemo") val service: Service = TypedActor(system).typedActorOf( TypedProps[ServiceImpl]() ) val req = Request("hello world!") service.request(req) onSuccess { case Response(response) => println(response) system.shutdown() } }
  27. Less Boilerplate require 'celluloid' class Greeter include Celluloid def greet(msg)

    puts msg end end Greeter.new.greet "hello world!"
  28. What is the Actor Model?

  29. Actor Model •Actors are computational entities that can receive messages

    •Each actor has a unique address •If you know an actor’s address, you can send it messages •Actors can create new actors
  30. How is that different from an object?

  31. Actors are concurrent Each actor has its own thread in

    Celluloid
  32. Actors are asynchronous

  33. Celluloid is an abstraction on top of the Actor Model

  34. Actor-based Concurrent Objects

  35. “Cells”

  36. A Contrived Example

  37. 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
  38. >> 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
  39. -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
  40. 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
  41. How?

  42. MAGIC

  43. Automatic locking?

  44. None
  45. None
  46. Locks are hard •Dining Philosophers Problem •Sleeping Barber Problem •Cigarette

    Smokers Problem
  47. OOP + Concurrency

  48. “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"
  49. OOP Tools Classes Inheritance Messages

  50. Concurrency Tools Threads Locks Queues

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

    +
  52. =

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

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

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

  56. Generalized Deadlock-free Synchronization

  57. None
  58. None
  59. None
  60. None
  61. None
  62. Pythons did it!

  63. None
  64. 1997

  65. None
  66. How does Celluloid work?

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

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

  69. # 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 ...
  70. Synchronous Calls

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

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

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

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

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

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

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

  78. Asynchronous Calls Kind of like “next tick”

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

  80. Futures

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

    &HOOXORLG 0DLOER[ )8785( &HOOXORLG )XWXUH &HOOXORLG5HVSRQVH 9$/8(" 9$/8(
  82. >> 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
  83. >> 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
  84. Basic Ingredients •Regular method calls •Async calls (“next tick”) •Futures

  85. Secret Sauce

  86. How do we prevent deadlocks?

  87. None
  88. None
  89. None
  90. None
  91. None
  92. Why do deadlocks happen?

  93. Waiting for something that never happens...

  94. ...instead of what’s important

  95. None
  96. How can we wait on everything at once?

  97. FIBERS!

  98. Communicating Sequential Processes Do one thing at a time

  99. Fibers Cheap suspendable/resumable execution context

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

    thing at a time +
  101. Internal Concurrency for Actors

  102. Don’t Block, Suspend to the Scheduler

  103. Example!

  104. 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)
  105. Hello Joe, I'm Mike Hello Mike, I'm Joe Output

  106. None
  107. But it is a cool story!

  108. Circular Call Graph 0LNH -RH JUHHW QDPH

  109. Communicating Sequential Processes Do one thing at a time

  110. DEADLOCK!

  111. 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)
  112. 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
  113. Waiting tasks suspend themselves so ready tasks can run

  114. Every method call creates a Fiber

  115. None
  116. Slow?

  117. Call Benchmark Core i7 2.0GHz (OS X 10.7.3) 16815/s (60µs)

    - JRuby 1.6.7 20899/s (50µs) - JRuby HEAD
  118. None
  119. JRuby fibers are threads :(

  120. Kilim Fast coroutines and actors for the JVM

  121. Kilim Fibers •JRuby GSoC project by Miloš Hadžić •“Weave” the

    JRuby runtime, JIT •Fast, cheap Fibers on JRuby! •50% chance of success :|
  122. What if a cell crashes?

  123. None
  124. None
  125. Fault Tolerance •Supervisors & Supervision Trees •“Fail early”, restart in

    a clean state •Do what Erlang does •No seriously, do what Erlang does
  126. Distributed Celluloid over 0MQ http://github.com/celluloid/dcell

  127. Cells are services

  128. DCell exposes them to the network

  129. Built on 0MQ •Fast, brokerless message queue •DCell uses push/pull

    sockets •Built on Celluloid::ZMQ/ffi-rzmq
  130. •Distributed process orchestration (e.g. Chef, Puppet) •Multi-tier Web Applications •Asynchronous

    background jobs Use Cases •Distributed process orchestration (e.g. Chef, Puppet) •Multi-tier Web Applications •Asynchronous background jobs
  131. Why do this? )URQW(QG )URQW(QG )URQW(QG 5(67 &OLHQW 5(67 &OLHQW

    5(67 &OLHQW 5(67 6HUYLFH 5(67 6HUYLFH 5(67 6HUYLFH 'RPDLQ 2EMHFW 'RPDLQ 2EMHFW 'RPDLQ 2EMHFW 'RPDLQ 2EMHFW 'RPDLQ 2EMHFW 'RPDLQ 2EMHFW 'RPDLQ 2EMHFW 'RPDLQ 2EMHFW 'RPDLQ 2EMHFW
  132. Instead of this? )URQW(QG )URQW(QG )URQW(QG 'RPDLQ 2EMHFW 'RPDLQ 2EMHFW

    'RPDLQ 2EMHFW 'RPDLQ 2EMHFW 'RPDLQ 2EMHFW 'RPDLQ 2EMHFW 'RPDLQ 2EMHFW 'RPDLQ 2EMHFW 'RPDLQ 2EMHFW
  133. "Objects can message objects transparently that live on other machines

    over the network, and you don't have to worry about the networking gunk, and you don't have to worry about finding them, and you don't have to worry about anything. It's just as if you messaged an object that's right next door." --Steve Jobs describing NeXT Portable Distributed Objects
  134. Distributed objects have mostly been a failure

  135. Distributed Object Failures •CORBA •SOAP •PDO •RMI •DRB

  136. Why?

  137. Not asynchronous Huge problem in distributed systems

  138. Not built on the Actor Model

  139. Actor Model is AWESOME

  140. Unifying abstraction for both concurrency and distribution

  141. Success!

  142. Example

  143. Two Node Cluster •Node #1 is “itchy” •Node #2 is

    “scratchy”
  144. “itchy” commands >> require 'dcell' => true >> DCell.start :id

    => 'itchy', :addr => 'tcp:// 127.0.0.1:7777' I, [2012-05-09T10:20:46.999000 #52416] INFO -- : Connected to itchy => #<Celluloid::Supervisor(DCell::Group):0x836> Note itchy is on port 7777
  145. “scratchy” commands >> require 'dcell' => true >> DCell.start :id

    => 'scratchy', :addr => 'tcp:// 127.0.0.1:7778', :directory => {:id => 'itchy', :addr => 'tcp://127.0.0.1:7777'} I, [2012-05-09T10:26:42.322000 #52555] INFO -- : Connected to itchy I, [2012-05-09T10:26:42.331000 #52555] INFO -- : Connected to scratchy => #<Celluloid::Supervisor(DCell::Group):0x838> Note scratchy is on port 7778
  146. itchy isn’t a “server” It’s an entry point

  147. Cluster coordination •Gossip protocol (multiple entry points coming soon!) •Paxos

    (coming soon!) •Zookeeper (coming soon... again!)
  148. Finding nodes •DCell::Node[‘itchy’] •DCell::Node.all •DCell.me

  149. Finding Remote Cells DCell::Node[‘itchy’][:info]

  150. >> info_service = DCell::Node['itchy'][:info] => #<Celluloid::Actor(DCell::InfoService:0x83c) @platform="java" @ruby_platform="jruby 1.6.7" @cpu_type="Intel(R)

    Core(TM) i7-2635QM CPU" @ruby_version="1.9.2" @ruby_engine="jruby" @cpu_vendor=:intel @distribution="Mac OS X 10.7.3 (11D50b)" @cpu_count=8 @hostname="wintermute.local" @cpu_speed=2.0 @os="darwin" @cpu_arch="x86_64" @os_version="11.3.0"> >> info_service.uptime => 42 >> info_service.load_averages => [1.08, 0.93, 0.84] >> info_service.distribution => "Mac OS X 10.7.3 (11D50b)" “scratchy” commands
  151. Defining DCell services

  152. require 'celluloid' class Greeter include Celluloid def hello "Hi" end

    end Example Cell
  153. >> Celluloid::Actor[:greeter] = Greeter.new => #<Celluloid::Actor(Greeter:0x7fc)> >> Celluloid::Actor[:greeter].hello => "Hi"

    Naming Cells
  154. >> Greeter.supervise_as :greeter => #<Celluloid::Supervisor:0x1fb> >> Celluloid::Actor[:greeter].hello => "Hi" Naming

    Cells
  155. >> node = DCell::Node['itchy'] => #<DCell::Node[itchy] @addr="tcp://127.0.0.1:7777"> >> node[:greeter].hello =>

    "Hi" Calling Cells
  156. All Celluloid features supported •Synchronous calls •Asynchronous calls •Futures

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

  158. 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)
  159. 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) Ruby servers on 1.9.3
  160. Websockets coming soon!

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

  162. Vaporware!

  163. Goals •Built out of parts of Rails •Webmachine for resources

    •Multithreaded development mode •Easy scatter/gather for SOA
  164. That’s all, folks!

  165. Twitter: @bascule Celluloid: celluloid.io Blog: unlimitednovelty.com

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