Multithreading and Parallel programming in Ruby

Giulio P.
2 min readNov 6, 2020

The Ruby standard library offers a Thread class which is aimed to perform tasks within multiple OS threads.

Unlike Fibers, the operating system chooses what to do with a thread, for example, creating a lot of threads will raise a ThreadError exception.

To run a task in a different Thread, one can use the Thread.new method, like the following code:

If you’re new with multithreading you would think this prints totally 2 lines to the standard output.

Instead, it only prints “Hello from the main thread!”, but why?

Because we haven’t stopped the “t1” thread yet.

If you want to wait the thread to stop executing, but not let it do it’s work, you can use one of either Thread#kill (or #exit or #terminate, which are aliases), or Thread.kill(t1).

If you want to wait until a thread’s task(s) are finished, you can simply use the method Thread#join.

Using Thread#join in the code above would the second thread do it’s work:

For further reading, see https://rubyapi.org/2.7/o/thread.

See also Fibers and Mutexes.

However, since Ruby’s virtual machine internals aren’t thread-safe, it has a GIL (GVL in MRI terminology).

What does that mean?

Consider a function calculating Integer#prime? in two (just 2 for simplicity) threads.

On my Raspberry Pi 4 (aarch64, 1.9GHz) this took 3 minutes in total, averaged to 47.31 seconds.

So, the single-threaded version should took around the double, right?

This runs a little faster than the multi-threaded one (mainly ’cause it doesn’t have to manage any extra thread).

In Ruby MRI, Threads run asynchronously but not in parallel.

So, if you want to have some code to run in parallel, you can use a Process, or a Ractor.

Writing multi-process code is easier than writing multithreading code, but processes are slower to create and destroy, processes can be zombie and sometimes they’re really confusing, on the other hand, threads are slower to code, faster to create and destroy, sometimes there’s a race condition and debugging can be very painful.

Ruby MRI 3.0 introduces Ractors, aimed to avoid race condition, hard-to-debug code, but also being able to run in parallel and being faster than processes.

Ractors are still experimental, but quite functioning.

This snippet runs as about the same time of the single-threaded one, but returns two values.

Conclusion: Threads run asynchronously but not in parallel, but Ractors run in Parallel.

--

--

Giulio P.
0 Followers

16 y/o Ruby, C, Lua, Crystal, Racket, Julia and Common Lisp programmer. https://phykos.github.io