Concurrent Ruby

Concurrent Ruby

A super quick intro to Concurrency in Ruby

Bcb446d5ebec71979786a22e56794c32?s=128

Gonzalo Maldonado

September 12, 2018
Tweet

Transcript

  1. Concurrent Ruby Gonzalo Maldonado Staff Eng @ Lumosity

  2. This is an introductory talk, so spend some time researching

    before using this code in production ^Not going to cover Celluloid / Actor model because they are long topics Warnings (part 1) → This is an Intro talk, no Production-ready code here. → This talk is not long enough to cover the Actor Model used in Celluloid and others
  3. Warnings (part 2) I'm not going to go deep on

    definitions. For proper definitions check out: → Working with Ruby Threads by Jessee Storimer → Java Concurrency in Practice by Brian Goetz (Mostly for the definitions) → Seven Concurrency Models in Seven Weeks by Paul Butcher
  4. Concurrency → Concurrency refers to the ability of different parts

    or units of a program, algorithm, or problem to be executed out-of-order or in partial order, without affecting the final outcome. https://en.wikipedia.org/wiki/ Concurrency(computerscience)
  5. In other words, it's a way for our code to

    "take turns completing a task", which can make it finish faster, even if we're doing only "one task at a time". We'll talk more about this later. Concurrency
  6. A thread is a light weight process (similar to a

    sub-process) that we can use to achieve concurrency. Threads are part of a process and they share the same resources of that process. Threads → Thread: light weight "sub-process" that we can use to achieve concurrency
  7. Imagine we have a method that takes a while to

    run. Here we're artificially slowing it down for sake of the example Example without Threads def add(arr) sleep(2) sum = 0 arr.each { |item| sum += item } sum end
  8. Example without Threads @arr = [1,2,3] @arr2 = [4,5,6] @arr3

    = [8,8,0] [@arr, @arr2, @arr3].each_with_index { |arr, i| puts "arr#{i} = #{add(arr)}" } Output: arr1 = 6 arr2 = 15 arr3 = 24 (benchmarked using time: real 0m6.039s)
  9. Example with Threads threads = (1..3).map do |1| Thread.new(i) do

    |i| arr = instance_variable_get("@arr#{i}") puts "arr#{i} = #{ add(arr) }" end end threads.each{ |t| t.join }
  10. By using threads, we have reduced the total execution time

    considerably. But notice how things got executed out of order. Also each individual call still slept for 2 seconds, so that's a number we cannot go under. Output: arr2 = 15 arr3 = 24 arr1 = 6 (benchmarked using time: real 0m2.038s)
  11. Thread SFATEY? → Threads look great! Why are we not

    using them everywhere? → Mostly because of S A F E T Y
  12. Thread Safety → Many common ruby idioms are not thread

    safe → Which means values cannot be deterministic or even available when threading
  13. n can return almost anything, including nil. # Not thread

    safe. @n = 0 3.times do Thread.start { 100.times { @n += 1 } } end # += is actually two operations that might end up being run out of order
  14. Threads is the most basic concurrent pattern we can use.

    There are others that are particularly popular on other programming languages Concurrency features other languages have built in (and like bragging about) → Promises in ES6 Javascript → Async also in ES6 → Channels in Go
  15. In these examples I will use the concurrent-ruby gem, but

    there are other implementations out there, or you can roll your own And yes Ruby can do them too! gem install concurrent-ruby
  16. Async → What: Feature: As a stateful, plain old Ruby

    class I want safe, asynchronous behavior → Why: So my long-running methods don't block the main thread
  17. Async → How: class Echo include Concurrent::Async def echo(msg) print

    "#{msg}\n" end end
  18. Here, the method stays synchronous until we use async or

    await ^Async means "fire and forget", we request something without waiting for it ^Await means we will "fire on a thread" but wait for this thread to be done. It might execute faster than the regular and it will guarantee when it gets executed. Async horn = Echo.new horn.echo('zero') # synchronous, not thread-safe # returns the actual return value of the method horn.async.echo('one') # asynchronous, non-blocking, thread-safe # returns an IVar in the :pending state horn.await.echo('two') # synchronous, blocking, thread-safe # returns an IVar in the :complete state
  19. Async → When Use them when you have some fire

    and forget feature like an info logger.
  20. Promises → What: Feature: As Ruby class I want to

    be able to chain concurrent operations → Why: Because we need to aggregate results from that chain
  21. Fulfill means return the value immediately. ^Then is the chaining

    of results ^We can even pick a different executor/ thread pool ^Execute asks the Promise to be run Promises → How: p = Concurrent::Promise .fulfill(20) .then { |result| result - 10 } .then { |result| result * 3 } .then(executor: different_executor){ |result| result % 5 }.execute
  22. Promises have an internal state to keep track of their

    execution. Also they keep track of rejections which is really helpful to spot where an issue happened Promises → How: p = Concurrent::Promise.execute{ "Hello, world!" } sleep(0.1) p.state #=> :fulfilled p.fulfilled? #=> true p.value #=> "Hello, world!" p = Concurrent::Promise.execute{ raise StandardError.new("Here comes the Boom!") } sleep(0.1) p.state #=> :rejected p.rejected? #=> true p.reason #=> "#<StandardError: Here comes the Boom!>"
  23. Promises → When: Promises are great for things like mixing

    responses from multiple APIs into a single response.
  24. Channel → What: Feature: As Ruby class I want to

    be able to queue operations, execute them and have a common place to reference them. → Why: To make code clearer, I want to have a place to send things into.
  25. Channel → How: puts "Main thread: #{Thread.current}" Concurrent::Channel.go do puts

    "Goroutine thread: #{Thread.current}" end # Main thread: #<Thread:0x007fcb4c8bc3f0> # Goroutine thread: #<Thread:0x007fcb4c21f4e8>
  26. Here we're using the << operator to push things into

    a channel. ^Select lets us pick the active channel ^Take lets us extract things from a channel Channel → How: c1 = Concurrent::Channel.new c2 = Concurrent::Channel.new Concurrent::Channel.go do sleep(2) c2 << 'two' end Concurrent::Channel.go do sleep(1) c1 << 'one' end 2.times do Concurrent::Channel.select do |s| s.take(c1) { |msg| print "received #{msg}\n" } s.take(c2) { |msg| print "received #{msg}\n" } end end
  27. Channel → How: # Output received one received two #

    Think of channels as a lightweight version of Resque/Sidekiq
  28. Concurrency is not Parallelism Concurrency is about dealing with lots

    of things at once. Parallelism is about doing lots of things at once. - Rob Pike
  29. Concurrency is not Parallelism → In Ruby the Global Interpreter

    Lock forces us to basically "run one thread at a time".
  30. Concurrency might not be the answer → You might not

    see a performance boost unless your code is slow or you are processing enough elements → Putting a code in a thread, won't make it run faster (it will just make it run somewhere else, which could make it run sooner) → Compiled code executes faster, so if you need to drastically speed up your code, use JRuby
  31. Choose the right tool for the job → 98% of

    the time putting deferrable code on a worker (Resque/Sidekiq) should be enough → Again JRuby or even (gasp) using another language might make sense for your problem → You can call code in other languages (C or Rust) from Ruby using FFI → Guaranteeing Thread safety is hard and complex, spend a lot of time testing
  32. References and Image credits → http://www.csc.villanova.edu/~mdamian/ threads/posixthreads.html → https://swiftludus.org/using-grand-central- dispatch-and-concurrency-in-ios/

    → https://www.slideshare.net/varunlalan/threads- in-ruby-basics → https://bearmetal.eu/theden/how-do-i-know- whether-my-rails-app-is-thread-safe-or-not/