$30 off During Our Annual Pro Sale. View Details »

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
Tweet

More Decks by Noah Gibbs

Other Decks in Programming

Transcript

  1. Concurrency in a New Way…
    that Kinda Works.
    Ruby Ractors
    I love questions.
    Have a question?
    Ask it!

    View Slide

  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.

    View Slide

  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!

    View Slide

  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.

    View Slide

  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.

    View Slide

  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.

    View Slide

  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?

    View Slide

  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.

    View Slide

  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?

    View Slide

  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

    View Slide

  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.

    View Slide

  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.

    View Slide

  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

    View Slide

  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.

    View Slide

  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.

    View Slide

  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.

    View Slide

  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.

    View Slide

  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.

    View Slide

  19. Questions?

    View Slide