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

Concurrency and Parallelism in Ruby

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.

Concurrency and Parallelism in Ruby

Facts and myths about concurrency and parallelisms in Ruby

Avatar for Nikita Chernov

Nikita Chernov

May 28, 2013
Tweet

Other Decks in Programming

Transcript

  1. Key points • Threading strategies • Ruby concurrency models •

    Running things parallel • Threaded servers • ActionController::Live
  2. IO intensive threads [:google, :amazon].each do |service| threads << Thread.new

    do call_remote_service service end end threads.map &:join
  3. CPU intensive threads n = 10_657_331_232_548_839 max = Math.sqrt(n).floor [

    (2..(max / 2).floor), (max - (max / 2).floor..max) ].each do |d| threads << Thread.new do is_prime? d, n end end threads.map(&:value).all?
  4. Locks and syncs class Account attr_reader :balance def initialize @balance

    = 0 end def deposit(amount) new_balance = @balance + amount sleep 3 # in progress... @balance = new_balance end end
  5. We’ll loose our money account = Account.new 2.times do Thread.new

    { account.deposit 300 } end # wait some time... account.balance # => 300
  6. Lock deposits class Account def initialize @balance = 0 @mutex

    = Mutex.new end def deposit(amount) @mutex.synchronize do # deposit money... end end end
  7. Mutex problems • Thread effect can be lost • Programmer

    controls everything • Easy to miss required locks • Easy to place too much locks • Pessimistic locking
  8. Software-Transactional Memory (def balance (ref 0)) (ref-set balance 100) IllegalStateException

    No transaction running clojure.lang.LockingTransaction.getEx (LockingTransaction.java:208)
  9. Software-Transactional Memory • Optimistic “locking” • STM informs about missing

    syncs • Rollbacks thread if changes occurred in shared state • jRuby can use Clojure STM and Akka STM • To many write collisions may make system stuck
  10. Actor model with Celluloid require 'celluloid' class Account include Celluloid

    attr_accessor :balance def deposit(amount) sleep 1 @balance = @balance.to_i + amount end end
  11. Runs in background account1 = Account.new account2 = Account.new account1.async.deposit

    300 account2.async.deposit 300 sleep 1 puts account1.balance # => 300 puts account2.balance # => 300
  12. Actor-based • Used in Erlang, Scala, Go • Celluloid for

    Ruby • Akka for jRuby • Single object being put in a thread • Executes “requests” one-by-one
  13. Reactor pattern require 'eventmachine' require 'em-http-request' def http_get(url) http =/

    EventMachine::HttpRequest.new(url).get http.callback { http.response_header.status } end EventMachine.run do http_get 'http://www.google.com/' end
  14. Adding Fibers require 'eventmachine' require 'em-http-request' require 'fiber' def http_get(url)

    f = Fiber.current http = EventMachine::HttpRequest.new(url).get http.callback { f.resume(http) } Fiber.yield end EventMachine.run do Fiber.new { page = http_get 'http://www.google.com/' puts page.response_header.status }.resume end
  15. Synchronous asynchronous require 'em-synchrony' require 'em-http-request' EventMachine.synchrony do page =

    EventMachine::HttpRequest.new( 'http://www.google.com' ).get puts page.response_header.status end
  16. Reactor pattern [conclusion] • Fibers are lighter than threads •

    Dependency on EM-libraries • Synchronous asynchronous
  17. FutureProof Exceptions describe 'exceptions' do before do thread_pool.submit 24, 0

    do |a, b| a / b end end context 'without asking for specific value' do it 'should not raise exceptions' do expect { thread_pool.perform; thread_pool.wait }.not_to raise_error end end context 'with asking for specific value' do it 'should raise an exception' do expect { thread_pool.perform; thread_pool.values[0] }.to raise_error(ZeroDivisionError) end end end
  18. Forks result = 5 pid = fork do result +=

    5 puts result # => 10 exit 0 end result += 100 Process.wait pid puts result # => 105
  19. Share some state read, write = IO.pipe result = 5

    pid = fork do result = result + 5 Marshal.dump result, write exit 0 end write.close result = read.read Process.wait pid puts Marshal.load result # => 10
  20. Expose object over the network require 'drb/drb' class Account attr_reader

    :balance def initialize(balance) @balance = balance end def withdraw(amount) if @balance >= amount @balance -= amount end end end DRb.start_service( 'druby://192.168.0.1:8787', Account.new(500) ) DRb.thread.join
  21. Linda • Coordinating language • Tuples and tuplespace • in

    - atomically reads and removes tuple • out - produces and writes tuple
  22. Rinda [main] require 'rinda/tuplespace' ts = Rinda::TupleSpace.new DRb.start_service( 'druby://localhost:12345', ts

    ) ts.write(['sqrt_q', 25]) _, _, result = ts.take(['sqrt_a', 25, nil]) puts result # => 5
  23. Pthread [executor] # Will execute passed code # on other

    computers in network Pthread::PthreadExecutor.new( '192.168.1.100:12345', 'tasks' )
  24. Unicorn • Master process • Forks workers • Each worker

    serves one request at a time • Each worker uses one database connection • Each worker has part of a Rails stack loaded into memory • 0 requests can be served on a free Heroku account
  25. Puma • Single process • Serves each request in an

    own thread • Each thread has only own context loaded in memory • Each thread requires an own database connection • Can server multiple requests at the same time on a free Heroku account
  26. repeat 3 curl h1ghlights.herokuapp.com • Started GET "/" for 212.142.101.97

    • Processing by HighlightsController#index • Started GET "/" for 212.142.101.97 • Started GET "/" for 212.142.101.97 • Processing by HighlightsController#index • Processing by HighlightsController#index • Completed 200 OK in 79ms • Completed 200 OK in 67ms • Completed 200 OK in 94ms
  27. Same for Resque vs Sidekiq • Multiple processes • Single

    threaded • Rails stack in each worker • Will run 0 jobs on a free Heroku account • Single process • Multiple threaded • Rails stack in the main thread only • Will run multiple jobs on a free Heroku account
  28. Test-drive on Heroku • Register “test-app” and “test-app-worker” • Set

    1 web dyno for “test-app” • Set 1 worker dyno for “test-app-worker” • Configure both to use the same database (e.g. MONGOHQ_URL) and Redis (e.g. REDISTOGO_URL) • More at: https://github.com/phoet/freemium
  29. Gemfile ruby '1.9.3', engine: 'rbx', engine_version: '2.0.0.rc1' gem 'rails', '4.0.0.rc1'

    gem 'puma' gem 'sidekiq', github: 'mperham/sidekiq', branch: 'rails4'
  30. Procfile web: bundle exec rails server puma -p $PORT -e

    $RACK_ENV worker: bundle exec sidekiq
  31. live/highlights_contoller.rb def index response.headers['Content-Type'] =/ 'text/event-stream' redis = Redis.new redis.subscribe('highlights')

    do |on| on.message do |event, data| data = JSON.parse(data) highlight = Highlight.find data['id'] response.stream.write( "data: #{{id: highlight._id}.to_json}\n\n" ) end end rescue IOError logger.info 'Stream closed' ensure redis.quit response.stream.close end
  32. Pros and Cons • current_user • All my code and

    all my models • Multiple Redis connections • Use app server connection pool • To many connection may bring down the whole app • "data: #{{id: highlight._id}.to_json }\n\n"
  33. Same with eventmachine require 'em-websocket' require 'em-hiredis' require 'em-synchrony' EM.run

    do @channel = EM::Channel.new @redis = EM::Hiredis.connect REDIS_URI @pubsub = @redis.pubsub @pubsub.subscribe 'highlights' @pubsub.on :message do |channel, message| @channel.push message end # ...
  34. Same with eventmachine # ... EventMachine::WebSocket.start(host: '0.0.0.0', port: 8080) do

    |ws| ws.onopen do sid = @channel.subscribe do |data| EM.synchrony do data = JSON.parse(data) highlight = Highlight.find(data['id']) data = { id: highlight.id } ws.send data.to_json end end ws.onclose do @channel.unsubscribe sid end end end end
  35. Pros and Cons • One Redis for app and one

    Redis for EM • If EM is down app is still working • Cleaner code • Sessions and users • Different codebase