Ruby threads worth it?
One of the most challenging things that I had as a software engineer, was dealing with a large amount of information at the same time. Then I realized that this happened with anyone, in any area. New sources of knowledge that complement themselves used to come up along the way.
In this article, I will bring a resume about Threads on Ruby, passing through the following topics:
- What are Threads?
- A brief resume of Ruby runtimes
- Threads on Ruby
- GIL
- Mutex
So chill bro, get your coffee and follow me.
What are Threads?
Threads are parts of the software that are executed separately from the main process of an application. Hard to catch? Let’s see an example.
We can compare threads with juggling. Imagine that the hand of the juggler represents one core of your CPU. The balls represent tasks that are being executed.
The juggler doesn’t keep the balls in his hand at the same time but takes turns with the balls. This concept could be understood as concurrency.
There are other ways to juggling. This time, with two hands(two cores from your CPU).
In this scenario, it can have tasks being executed at the same time. This way we can see the concept of parallelism.
A brief resume of Ruby Runtimes
Ruby was created by Yukihiro Matsumoto, or “Matz”, in Japan in the mid-1990s. It was designed for programmer productivity with the idea that programming should be fun for programmers.
“I would categorize Ruby as a productive programming language. Productivity is one of the biggest, primary goals of Ruby. I designed Ruby for humans, not for machines”... — Yukihiro Matsumoto
As the language grows older new implementations of Ruby interpreter show up.
The most popular Ruby interpreter is the Ruby MRI, developed in C by the creator of Ruby.
JRuby is a version of Ruby implemented on top of the Java Virtual Machine (JVM), it was originally created by Jan Arne Petersen, in 2001.
Rubinius is an alternative Ruby implementation created by Evan Phoenix. Based loosely on the Smalltalk-80 Blue Book design, Rubinius seeks to provide a rich, high-performance environment for running Ruby code. Like JRuby, Rubinius includes a JIT compiler, better memory management, and a more mature virtual machine than Ruby MRI.
There are other implementations of Ruby interpreter, but for now, let's focus on these implementations.
Threads on Ruby
Creating threads in Ruby is actually pretty simple:
Thread.new { "A new thread!!" }
But multi-thread programming isn’t that simple. We need to pay attention to these questions:
- Where and when I’m going to spawn threads?
- How many?
- What about thread-safety?
What we need to understand is: threads have a shared address space. This is a fancy way of saying that multiple threads will share the same references in memory(variables), as well as the compiled source code.
The operating system has a thread scheduler, that guarantees that all threads have fair access to the system resources. In order to provide fair access, the thread scheduler can ‘pause’ a thread at any time. At some point, it can unpause the thread restoring it to its previous state. This is known as context switching.
We can have more than one thread trying to do one task and modifying the same variables. So who manages it?
GIL
GIL (Global Interpreter Lock) or GVL (Global VM Lock), act as a lock around Ruby code.
It allows concurrent execution of Ruby code but prevents parallel execution.
Each instance of MRI has a GIL. If some of these MRI processes spawn multiple threads, GIL comes in action, preventing parallel execution.
With MRI implementation, inconsistencies will be rare on the result, but with JRuby or Rubinius you can find it frequently.
shared_array = Array.new10.times.map do
Thread.new do
1000.times do
shared_array << nil
end
end
end.each(&:join)puts shared_array.sizeResults:## with Ruby MRI -> 1000
## with Jruby -> 7521
## with Rubinius -> 8541
Executing the code above, we notice a few inconsistencies in other Runtimes that don’t use GIL. In a globalized world, where information is a valuable thing, inconsistencies like this one could cause big problems. Imagine a bank application losing data and money, mistaken mathematical calculus on an airport system, or even miscalculation on the brand new Tesla automated cars. These mistakes cannot happen in big systems.
Mutex
In order to protect our execution, we need something that controls a shared state between threads.
Just like a traffic light controls car flow, a mutex can control threads flow, protecting our variables.
shared_array = Array.new
mutex = Mutex.new10.times.map do
Thread.new do
1000.times do
mutex.lock
shared_array << nil
mutex.unlock
end
end
end.each(&:join)puts shared_array.sizeResults:## with Ruby MRI -> 1000
## with Jruby -> 1000
## with Rubinius -> 1000
Pretty easy right? Well, for this little piece of code we can say it is. But to keep control of mutexes in a big application, it ain’t a simple thing to do. Your application will keep growing and growing, and at some point, you will mess things up.
At the end of the day, you need to question yourself, why do I need threads in my application? Using threads with Ruby and getting big results is not a trivial thing. Doing this in the wrong way could be so frustrating.
If you need some feature that needs to be executed in an asynchronous way, maybe you can consider looking at some messaging platforms, such as RabbitMQ or Apache Kafka.
There are other programming languages that provide you a better approach to deal with threads and use all hardware resources, without the need for controlling thread at such low level. On Gympass we use AkkaHttp + Scala, which provides high performance and scalability to our microservices.
But if you are a Ruby fanboy(just like me haha), I suggest you take a look at Elixir, which has concepts from Erlang(BEAM VM), actor model, immutability and so on. And it has a syntax pretty similar to Ruby.
To more information about this subject:
- Working with Ruby Threads, Jesse Storimer
- For Rubinius and JRuby details of implementation, Ruby under a microscope, Pat Shaughnessy
- https://github.com/rubinius/rubinius/
- https://www.jruby.org/
- https://www.youtube.com/watch?v=dP4U1yI1WZ0