Concurrency: Rubies, plural

1,931 views

Published on

talk about concurrency in Ruby, different concurrency models, examples from other languages.

Published in: Technology
0 Comments
3 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
1,931
On SlideShare
0
From Embeds
0
Number of Embeds
2
Actions
Shares
0
Downloads
29
Comments
0
Likes
3
Embeds 0
No embeds

No notes for slide

Concurrency: Rubies, plural

  1. 1. Concurrency: Rubies, Plural Elise Huard & Eleanor McHugh RubyConf 2010 Friday 12 November 2010
  2. 2. manifesto concurrency matters Friday 12 November 2010
  3. 3. multicore it’s a revolution in mainstream computing and you want to exploit it in Ruby Friday 12 November 2010
  4. 4. MULTIPROCESSOR/MULTICORE Friday 12 November 2010
  5. 5. NETWORK ON CHIP (50..96..100 CORES) Friday 12 November 2010
  6. 6. diminishing returns • communication takes finite time • so doubling processors never doubles a system’s realworld performance • realtime capacity ~70% theoretical capacity • or less!!! • independence = performance Friday 12 November 2010
  7. 7. “... for the first time in history, no one is building a much faster sequential processor. If you want your programs to run significantly faster (...) you’re going to have to parallelize your program.” Hennessy and Patterson “Computer Architectures” (4th edition, 2007) Friday 12 November 2010
  8. 8. concurrency why it really matters Friday 12 November 2010
  9. 9. an aide to good design • decouples independent tasks • encourages data to flow efficiently • supports parallel execution • enhances scalability • improves program comprehensibility Friday 12 November 2010
  10. 10. Friday 12 November 2010
  11. 11. Friday 12 November 2010
  12. 12. Friday 12 November 2010
  13. 13. finding green pastures adopting concurrency idioms from other languages Friday 12 November 2010
  14. 14. victims of choice Erlang Actors Go Concurrent Sequential Processes Clojure Software Transactional Memory Icon Coexpressions Friday 12 November 2010
  15. 15. coroutines synchronising via transfer of control Friday 12 November 2010
  16. 16. icon • a procedural language • with a single thread of execution • generators are decoupled coexpressions • and goal-directed evaluation • creates flexible flow-of-control Friday 12 November 2010
  17. 17. icon coexpressions procedure main() n := create(seq(1)10) s := create(squares()) c := create(cubes()) while write(@n,“t”, @s,“t”, @c) end procedure seq(start) repeat { suspend start start += 1 } end procedure squares() odds := 3 sum := 1 repeat { suspend sum sum +:= odds odds +:= 2 } end procedure cubes() odds := 1 sum := 1 i := create(seq(2)) repeat { suspend sum sum +:= 1 + (odds * 6) odds +:= @i } end 1 1 1 2 4 8 3 9 27 4 16 64 5 25 125 6 36 216 7 49 343 8 64 512 9 81 729 10 100 1000 Friday 12 November 2010
  18. 18. ruby fibers • coexpressions • bound to a single thread • scheduled cooperatively • the basis for enumerators • library support for full coroutines Friday 12 November 2010
  19. 19. ruby coexpressions def seq start = 1 Fiber.new do loop do Fiber.yield start start += 1 end end end n = seq() s = Fiber.new do sum, odds = 1, 3 loop do Fiber.yield sum sum += odds odds += 2 end end c = Fiber.new do sum, odds, i = 1, 1, seq(2) loop do Fiber.yield sum sum += 1 + (odds * 6) odds += i.resume end end 10.times do puts “#{n.resume}t#{s.resume}t#{c.resume}” end 1 1 1 2 4 8 3 9 27 4 16 64 5 25 125 6 36 216 7 49 343 8 64 512 9 81 729 10 100 1000 Friday 12 November 2010
  20. 20. ruby coroutines require 'fiber' def login Fiber.new do |server, name, password| puts "#{server.transfer(Fiber.current)} #{name}" puts "#{server.transfer name} #{password}" puts “login #{server.transfer password}” end end def authenticate Fiber.new do |client| name = client.transfer "name:" password = client.transfer "password:" if password == "ultrasecret" then client.transfer "succeeded" else client.transfer "failed" end end end login.transfer authenticate, "jane doe", "ultrasecret" login.transfer authenticate, "john doe", "notsosecret" name: jane doe password: ultrasecret login succeeded name: john doe password: notsosecret login failed Friday 12 November 2010
  21. 21. icon revisited procedure powers() repeat { while e := get(queue) do write(e,“t”, e^ 2,“t”, e^ 3) e @&source } end procedure process(L) consumer := get(L) every producer := !L do while put(queue, @producer) do if *queue > 3 then @consumer @consumer end global queue procedure main() queue := [] process{ powers(), 1 to 5, seq(6, 1)5 } end 1 1 1 2 4 8 3 9 27 4 16 64 5 25 125 6 36 216 7 49 343 8 64 512 9 81 729 10 100 1000 Friday 12 November 2010
  22. 22. a ruby equivalent require 'fiber' def process consumer, *fibers q = consumer.transfer(Fiber.current) fibers.each do |fiber| while fiber.alive? q.push(fiber.resume) q = consumer.transfer(q) if q.length > 3 end end consumer.transfer q end powers = Fiber.new do |caller| loop do caller.transfer([]).each do |e| puts "#{e}t#{e ** 2}t#{e ** 3}" rescue nil end end end low_seq = Fiber.new do 5.times { |i| Fiber.yield i + 1 } nil end high_seq = Fiber.new do (6..10).each { |i| Fiber.yield i } end process powers, low_seq, high_seq 1 1 1 2 4 8 3 9 27 4 16 64 5 25 125 6 36 216 7 49 343 8 64 512 9 81 729 10 100 1000 Friday 12 November 2010
  23. 23. processes the traditional approach to concurrency Friday 12 November 2010
  24. 24. language VM OS (kernel processes, other processes) Your program multicore - multiCPU Friday 12 November 2010
  25. 25. processes + threads Process 2 RAMmemory space Process 1 thread1 scheduler (OS) CPU CPU memory space thread2 t1 t2 t3 Friday 12 November 2010
  26. 26. cooperative preemptive active task has full control scheduler controls task activity runs until it yields control switches tasks automatically tasks transfer control directly a task can still yield control within one thread several threads schedulers Friday 12 November 2010
  27. 27. 3 threads, 2 cores 1 2 3 1 1 2 3 1 2 3 core1 core2 core1 core2 Friday 12 November 2010
  28. 28. process thread address space kernel resources scheduling communication control private shared private + shared shared kernel varies IPC via kernel in process children in process feature comparison Friday 12 November 2010
  29. 29. process creation • unix spawns • windows cuts from whole cloth • ruby wraps this many ways • but we’re mostly interested in fork Friday 12 November 2010
  30. 30. fork • creates a new process • in the same state as its parent • but executing a different code path • forking is complicated by pthreads • and ruby’s default garbage collector Friday 12 November 2010
  31. 31. pipes • creates an I/O channel between processes • unnamed pipes join two related processes • posix named pipes • live in the file system • have user permissions • persist independently of processes Friday 12 November 2010
  32. 32. def execute &block child_input, parent_input = IO.pipe pid = fork do child_input.close result = block.call parent_input.write result.to_json parent_input.close end parent_input.close sorted = JSON.parse child_input.read child_input.close Process.waitpid pid return sorted end forking Friday 12 November 2010
  33. 33. context switching Operating System Benchmark Operation Time (ms) LinuxLinuxLinuxLinuxLinux Windows NTWindows NTWindows NTWindows NTWindows NT spawn new process fork() / exec() 6,000 clone current process fork() 1,000 spawn new thread pthread_create 0,300 switch current process sched_yield() 0,019 switch current thread sched_yield() 0,019 spawn new process spawnl() 12,000 clone current process N/A --- spawn new thread pthread_create() 0,900 switch current process Sleep(0) 0,010 switch current thread Sleep(0) 0,006 C Benchmarks by Gregory Travis on a P200 MMX http://cs.nmu.edu/~randy/Research/Papers/Scheduler/ C Benchmarks by Gregory Travis on a P200 MMX http://cs.nmu.edu/~randy/Research/Papers/Scheduler/ C Benchmarks by Gregory Travis on a P200 MMX http://cs.nmu.edu/~randy/Research/Papers/Scheduler/ C Benchmarks by Gregory Travis on a P200 MMX http://cs.nmu.edu/~randy/Research/Papers/Scheduler/ Friday 12 November 2010
  34. 34. shared state hurts • non-determinism • atomicity • fairness/starvation • race conditions • locking • transactional memory Friday 12 November 2010
  35. 35. semaphores • exist independently of processes • provide blocking access • allowing processes to be synchronised • nodes in the file system • usable from Ruby with syscall Friday 12 November 2010
  36. 36. synchronising processes require ‘dl’ require ‘fcntl’ libc = DL::dlopen ‘libc.dylib’ open = libc[‘sem_open’,‘ISII’] try_wait = libc[‘sem_trywait’,‘II’] wait = libc[‘sem_wait’,‘II’] post = libc[‘sem_post’,‘II’] close = libc[‘sem_close’,‘II’] process 1 s = open.call(“/tmp/s”, Fcntl::O_CREAT, 1911)[0] wait.call s puts “locked at #{Time.now}” sleep 50 puts “posted at #{Time.now}” post.call s close.call s process 2 s = open.call(“/tmp/s”) t = Time.now if try_wait.call(s)[0] == 0 then puts “locked at #{t}” else puts “busy at #{t}” wait.call s puts “waited #{Time.now - t} seconds” end locked at Thu May 28 01:03:23 +0100 2009 busy at Thu May 28 01:03:36 +0100 2009 posted at Thu May 28 01:04:13 +0100 2009 waited 47.056508 seconds Friday 12 November 2010
  37. 37. complexity • file locking • shared memory • message queues • transactional data stores Friday 12 November 2010
  38. 38. threads the popular approach to concurrency Friday 12 November 2010
  39. 39. threads under the hood from http://www.igvita.com/2008/11/13/concurrency-is-a-myth-in-ruby/ @igrigorik Friday 12 November 2010
  40. 40. the global lock • compatibility for 1.8 C extensions • only one thread executes at a time • scheduled fairly with a timer thread • 10 μs for Linux • 10 ms for Windows Friday 12 November 2010
  41. 41. synchronisation • thread groups • locks address race conditions • mutex + monitor • condition variable • deadlocks • livelocks Friday 12 November 2010
  42. 42. threads + sockets require 'socket' require 'thread' class ThreadGroup def all_but_me &block list. delete_if { |t| t.name == Thread.current.name }.each { |t| yield t } end def select *names, &block list.delete_if { |t| !names.include? t.name }.each { |t| yield t } end end class Thread def name= n raise ArgumentError if key? :name self[:name] = n self[:Q] = Queue.new end def send message self[:Q].enq message end def bind_transmitter socket raise ArgumentError if key? :XMIT self[:socket] = socket self[:XMIT] = Thread.new(socket, self[:Q]) do |s, q| loop do case message = q.deq when :EXIT: Thread.current.exit else s.puts message end Thread.pass end end end def disconnect send "Goodbye #{name}" send :EXIT self[:XMIT].join self[:socket].close exit end end Friday 12 November 2010
  43. 43. threads + sockets class ChatServer < TCPServer def initialize port @receivers = ThreadGroup.new @transmitters = ThreadGroup.new @deceased = ThreadGroup.new super end def register name, socket @receivers.add(t = Thread.current) t.name = name @transmitters.add t.bind_transmitter(socket) broadcast "#{name} has joined the conversation" t.send "Welcome #{name}" end def connect socket loop do begin socket.print "Please enter your name: " register socket.readline.chomp).downcase, socket break rescue ArgumentError socket.puts "That name is already in use" end end end def send message, name = Thread.current.name @receivers.select(name) { |t| t.send message } end def broadcast message @receivers.all_but_me { |t| t.send message } end def listen socket t = Thread.current loop do message = socket.readline.chomp case message.downcase when "bye" raise EOFError when "known" list_known_users when "quit" raise SystemExit else broadcast "#{t.name}: #{message}" end end end def run while socket = accept Thread.new(socket) do |socket| begin connect socket listen socket rescue SystemExit broadcast "#{Thread.current.name} terminated this conversation" broadcast :EXIT send :EXIT @receivers.all_but_me { |t| t.transmitter.join } Kernel.exit 0 rescue EOFError disconnect end end end end end ChatServer.new(3939).run Friday 12 November 2010
  44. 44. the macruby twist • grand central dispatch • uses an optimal number of threads • state is shared but not mutable • object-level queues for atomic mutability Friday 12 November 2010
  45. 45. clojure lisp dialect for the JVMlisp dialect for the JVM refs software transactional memory agents independent, asynchronous change vars in-thread mutability check outTim Bray’s Concur.next seriescheck outTim Bray’s Concur.next series Friday 12 November 2010
  46. 46. parallel banking (ns account)   ; ref   (def transactional-balance (ref 0))   ; transfer: within a transaction   (defn parallel-transfer [amount]      (dosync         (alter transactional-balance transfer amount)))   ; many threads adding 10 onto account   (defn parallel-stm [amount nthreads]      (let [threads (for [x (range 0 nthreads)] (Thread. #(parallel-transfer amount)))]         (do            (doall (map #(.start %) threads))            (doall (map #(.join %) threads))))      @transactional-balance) Friday 12 November 2010
  47. 47. ruby can do that too require 'clojure' include Clojure def parallel_transfer(amount)   Ref.dosync do     @balance.alter {|b| b + amount }   end end def parallel_stm(amount, nthreads)   threads = []   10.times do     threads << Thread.new do       parallel_transfer(amount)     end   end   threads.each {|t| t.join }   @balance.deref end @balance = Ref.new(0) puts parallel_stm(10,10) Friday 12 November 2010
  48. 48. enumerables everyday ruby code which is naturally concurrent Friday 12 November 2010
  49. 49. vector processing • collections are first-class values • single instruction multiple data • each datum is processed independently • successive instructions can be pipelined • so long as there are no side-effects Friday 12 November 2010
  50. 50. map/reduce • decompose into independent elements • process each element separately • use functional code without side-effects • recombine the elements • intrinsically suited to parallel execution Friday 12 November 2010
  51. 51. a two-phase operation concurrent sequential (0..5). to_a. each { |i| puts i } x = 0 (0..5). to_a. each { |i| x = x + i } (0..5). to_a. map { |i| i ** 2 } (0..5). to_a. inject { |sum, i| sum + i } Friday 12 November 2010
  52. 52. parallel require 'brute_force' require 'parallel' # can be run with :in_processes as well mapped = Parallel.map((0..3).to_a, :in_threads => 4) do |num|   map("english.#{num}") # hash the whole dictionary end hashed = "71aa27d3bf313edf99f4302a65e4c042" puts reduce(hashed, mapped) # returns “zoned” Friday 12 November 2010
  53. 53. algebra, actors + events synchronising concurrency via communication Friday 12 November 2010
  54. 54. process calculi • mathematical model of interaction • processes and events • (a)synchronous message passing • named channels with atomic semantics Friday 12 November 2010
  55. 55. go • statically-typed compiled systems language • class-free object-orientation • garbage collection • independent lightweight coroutines • implicit cross-thread scheduling • channels are a first-class datatype Friday 12 November 2010
  56. 56. package main import "syscall" func (c *Clock) Start() { if !c.active { go func() { c.active = true for i := int64(0); ; i++ { select { case status := <- c.Control: c.active = status default: if c.active { c.Count <- i } syscall.Sleep(c.Period) } } }() } } type Clock struct { Period int64 Count chan int64 Control chan bool active bool } func main() { c := Clock{1000, make(chan int64), make(chan bool), false} c.Start() for i := 0; i < 3; i++ { println("pulse value", <-c.Count, "from clock") } println("disabling clock") c.Control <- false syscall.Sleep(1000000) println("restarting clock") c.Control <- true println("pulse value", <-c.Count, "from clock") } produces: pulse value 0 from clock pulse value 1 from clock pulse value 2 from clock disabling clock restarting clock pulse value 106 from clock a signal generator Friday 12 November 2010
  57. 57. homework • write a signal generator in ruby • hints: • it’s very easy • look at the thread + socket example • use atomic queues • and yes, ruby grok’s csp Friday 12 November 2010
  58. 58. actor model •named actors •fully independent of each other •asynchronous message passing •and no shared state Friday 12 November 2010
  59. 59. erlang • implements actors with green processes • efficient SMP-enabledVM • functional language • hot code loading • fault-tolerant © ericsson 2007 Friday 12 November 2010
  60. 60. erlang -module(brute_force). -import(plists). -export(run/2). map(FileName) ->     {ok, Binary} = file:read_file(FileName),     Lines = string:tokens(erlang:binary_to_list (Binary), "n"),     lists:map(fun(I) -> {erlang:md5(I), I} end, Lines). reduce(Hashed, Dictionary) ->     dict:fetch(Hashed, Dictionary). run(Hashed, Files) ->     Mapped = plists:map(fun(I) -> map(I) end, Files),     Values = lists:flatten(Mapped),     Dict = dict:from_list(Values),     reduce(Hashed, Dict). Friday 12 November 2010
  61. 61. erlang pmap(F, L) ->     S = self(),     Pids = lists:map(fun(I) -> spawn(fun() -> pmap_f(S, F, I) end) end, L),     pmap_gather(Pids). pmap_gather([H|T]) ->     receive         {H, Ret} -> [Ret|pmap_gather(T)]     end; pmap_gather([]) ->     []. pmap_f(Parent, F, I) ->     Parent ! {self(), (catch F(I))}. Friday 12 November 2010
  62. 62. rubinius actors • implemented as ruby threads • each thread has an inbox • cross-VM communication Friday 12 November 2010
  63. 63. rubinius actors require 'quick_sort' require 'actor' class RbxActorSort " def execute(&block) " " current = Actor.current " " Actor.spawn(current) {|current| current.send (block.call) } " " Actor.receive # could have filter " end end puts q = QuickSort.new([1,7,3,2,77,23,4,2,90,100,33,2,4], RbxActorSort).sort Friday 12 November 2010
  64. 64. ruby revactor • erlang-like semantics • actor spawn/receive • filter • uses fibers for cooperative scheduling • so only works on ruby 1.9 • non-blocking network access in ruby 1.9.2 Friday 12 November 2010
  65. 65. promises + futures • abstraction for delayed execution • a promise is calculated in parallel • a future is calculated on demand • caller will block when requesting result • until that result is ready Friday 12 November 2010
  66. 66. ruby futures Lazy.rb gem (@mentalguy) require 'lazy' require 'lazy/futures' def fib(n)   return n if (0..1).include? n   fib(n-1) + fib(n-2) if n > 1 end puts "before first future" future1 = Lazy::Future.new { fib(40) } puts "before second future" future2 = Lazy::Future.new { fib(40) } puts "and now we're waiting for results ... getting futures fulfilled is blocking" puts future1 puts future2 Friday 12 November 2010
  67. 67. we didn’t cover • tuple spaces (Rinda) • event-driven IO • petri nets • ... Friday 12 November 2010
  68. 68. reinventing? some of these problems have been solved before ... Friday 12 November 2010
  69. 69. to surmise • beware of shared mutable state • but: sane ways to handle concurrency • they are all possible in Ruby Friday 12 November 2010
  70. 70. fun! Friday 12 November 2010
  71. 71. http://www.delicious.com/elisehuard/concurrency http://www.ecst.csuchico.edu/~beej/guide/ipc/ http://wiki.netbsd.se/kqueue_tutorial http://www.kegel.com/c10k.html Elise Huard @elise_huard http://jabberwocky.eu Eleanor McHugh @feyeleanor http://slides.games-with-brains.net further reading Friday 12 November 2010

×