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

Concurrent Ruby

Concurrent Ruby

A super quick intro to Concurrency in Ruby

Gonzalo Maldonado

September 12, 2018
Tweet

More Decks by Gonzalo Maldonado

Other Decks in Programming

Transcript

  1. Concurrent
    Ruby
    Gonzalo Maldonado
    Staff Eng @ Lumosity

    View Slide

  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

    View Slide

  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

    View Slide

  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)

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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)

    View Slide

  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 }

    View Slide

  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)

    View Slide

  11. Thread SFATEY?
    → Threads look great! Why are we
    not using them everywhere?
    → Mostly because of S A F E T Y

    View Slide

  12. Thread Safety
    → Many common ruby idioms are not thread safe
    → Which means values cannot be deterministic or
    even available when threading

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  17. Async
    → How:
    class Echo
    include Concurrent::Async
    def echo(msg)
    print "#{msg}\n"
    end
    end

    View Slide

  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

    View Slide

  19. Async
    → When
    Use them when you have some fire and forget feature like an info logger.

    View Slide

  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

    View Slide

  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

    View Slide

  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 #=> "#"

    View Slide

  23. Promises
    → When:
    Promises are great for things like mixing responses from multiple APIs into a single response.

    View Slide

  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.

    View Slide

  25. Channel
    → How:
    puts "Main thread: #{Thread.current}"
    Concurrent::Channel.go do
    puts "Goroutine thread: #{Thread.current}"
    end
    # Main thread: #
    # Goroutine thread: #

    View Slide

  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

    View Slide

  27. Channel
    → How:
    # Output
    received one
    received two
    # Think of channels as a lightweight version of Resque/Sidekiq

    View Slide

  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

    View Slide

  29. Concurrency is not Parallelism
    → In Ruby the Global Interpreter Lock forces us to
    basically "run one thread at a time".

    View Slide

  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

    View Slide

  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

    View Slide

  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/

    View Slide