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

Ractors in Ruby 3

Ractors in Ruby 3

What are Ruby Ractors? How are they in Ruby 3.0.0? What are the pitfalls? How's the speed?

This presentation was given at a ScotRUG meetup in March of 2021.

Noah Gibbs

March 14, 2021

More Decks by Noah Gibbs

Other Decks in Programming


  1. Concurrency in a New Way… that Kinda Works. Ruby Ractors

    I love questions. Have a question? Ask it!
  2. What Is a Ractor? • Before we can answer that,

    we have to discuss the GIL (a.k.a. GVL) - the lock that keeps us from running Ruby code in multiple threads at once.
  3. What's a GVL? • The Global VM Lock is a

    lock that Ruby has to acquire before running Ruby code. There is only one lock*. Thus, "Global." * Until Ruby 3.0… My GVL… Mine!
  4. Want to Do More Than One Thing? Not In This

    Process, Mister. • Ruby can do multiple things at once. Not everything grabs the GVL. • "Free" activities include waiting on the database or network, plus many calculations performed by C Native Extensions. • Other than that, you get one activity per process.
  5. Ruby Threads? Weird. • Ruby threads aren't like other languages

    on purpose. Matz wants threads to break only between lines of Ruby. • That requires a GVL. Matz is frequently quoted as hating threads and wishing he hadn't added them to Ruby. I'm not kidding.
  6. Why Hate Threads? • Thread sync is hard to learn

    and hard even for the pros. • How to avoid sync issues while allowing concurrency? • Erlang-style copying; most objects belong to one thread. • And that's how Ractors work.
  7. Yeah, But How? • One GVL (now Global Ractor Lock)

    per Ractor - every thread lives in a specific parent Ractor • Never use Ractors? You get the old behaviour. • Replace Threads with Ractors? You get full concurrency. • But how do they sync?
  8. Sync, Sanc, Sunc • Each Ruby object now belongs to

    a Ractor - can't touch other Ractors' stuff. • Can pass objs via channels, like in Go.
  9. Annoying But Optional • This is hard for shared global

    state (e.g. Random.) A lot of libraries will need restructuring. For now, you can't use them from non-primary Ractors*. • Never add a Ractor? You can ignore all of this. * "Is Rails going to support Ractors now?" Not soon. Any idea how much shared global state Rails has?
  10. What's the API? Ractors get values and return them: ractor1

    << :item # Send item in ret_val = ractor1.take # Get item out # Inside ractor next_item = Ractor.recv Ractor.yield :my_return_val
  11. Moar Code # A simple Ractor that checks whether numbers

    from N to N+99 are prime # and returns an integer bit-vector of the true/false results Ractor.new(pipe) do |pipe| while n = pipe.take bools = (n..(n+99)).map { |nn| nn.prime? } p_int = bools.inject(0) { |total, b| total * 2 + (b ? 1 : 0) } Ractor.yield [n, p_int ] end end This is the same benchmark code I use later, though I wrap it in slightly different Ractor read/write access patterns.
  12. How Fast Are They? • The whole point of Ractors

    is speed. • They can use all your cores, not just one. • Ractors can use far less memory than processes.
  13. Theoretical Speed • At best, N Ractors are as fast

    as N processes* (max speed at 100% CPU all cores.) • Ractors have implementation limits that slow them down. • With no memory limits, right now: Processes > Ractors > Threads in nearly all cases. * if memory isn't an important limit
  14. Practical Speed I wrote some Ractor benchmarks. They run… With

    occasional hangs. How's the speed on a 4-vCPU Linode? Not good. There are test benchmarks where Ractors work as claimed but I have trouble duplicating those results. Here's a small benchmark (5 workers, 1,000 msgs/worker) with several Ractor access patterns.
  15. Which Code? Not so good. "Single" is the single-pass version.

    All calculation means threads are slower than single. Multiple processes ("fork") is fast b/c no GVL. All Ractor code does worse than threading, not better.
  16. Maybe Setup is Bad? But perhaps the startup/setup time is

    bad and ractors get better with more messages? Here we try 10 workers and 10,000 msgs/ worker (I skip "single" here, it's about the same as "thread.") Basically: not that much worse than threads, but never better than threads.
  17. Takeaways • Ractors are currently hard to use. There might

    be some simple way to make this fast. But I tried a lot of things with no luck. • Ractors hang on reasonable-looking use cases and you can't use a lot of Ruby standard library code.
  18. Takeaways • If you use Ractors, try to start from

    core benchmark code. That's how I got this far. • On a Mac they're even less stable. • The little printed warning about Ractors being experimental? They mean it.