Multithreaded Ruby — Synchronization, Race Conditions and Deadlocks

Threaded programming is hard. Mysterious, non-deterministic bugs and other issues can haunt you. What can we do about this?

Photo by yip vick on Unsplash

Threads, even on MRI Ruby, can lead to race conditions and deadlocks which can crash your service and are difficult to identify because of their non-deterministic nature. So, let’s take a look at multithreaded synchronization and the evils that come with it.

In this article we will mainly discuss about the atomicity problem of concurrent programming, but first let’s take a look at some of the evils that are often mentioned when talking about concurrent programming.

What is a deadlock?

A deadlock happens when two or more threads are trying to access the same shared resource, if the resource is guarded under a synchronization mechanism. Here is an example:

Why guard a resource in the first place?

In two words, Race Conditions. If we are using threads because we want to utilize our CPU cores as much as possible¹, we need to make the threads work together somehow. They may need to communicate with each other or, as in the example above, they may need to change the state of a shared object. Then we need to make sure that we won’t lose data.

Sometimes the GIL saves us so we don’t have to synchronize the access to a shared resource. Here is an example where the GIL saves us:

But this doesn’t hold always true:

This code will result in different outputs depending on your luck — remember this is non-deterministic. When I did run the code in the IRB console for the first time I received the message “Opening the door” only once, but trying several times I got the message more often — once even four times.

This clearly demonstrates that the GIL doesn’t make your Ruby code thread safe by default.

Synchronization issues in other Ruby implementations

JRuby

With JRuby we stumble into this kind of synchronization errors even faster. JRuby doesn’t have a GIL so every change of a shared resource can be done from different threads simultaneously and we always have this problem.

TruffleRuby

TruffleRuby is experimenting with something special. They want to implement an automatic synchronization. The important part here is that this would be done only if the service is using threads, so that the single threaded performance stays high. So, TruffleRuby would save us from doing the synchronization manually.

Can a deadlock only happen in a memory shared multithreaded mmodel?

Recently, there has been quite a buzz around new languages that implement concurrency in different ways. Elixir, Go, Closure, Kotlin and Scala with the Akka framework are just a few gems that appeared in this time.

And although it is true that concurrency is easier without threads, different approaches won’t solve everything. Here is a blog post that describes a deadlock in a CSP based concurrency approach.

How to avoid deadlocks

Disclaimer: the strategies presented here are in no way complete. I just present some possibilities that I have found are practical for some circumstances.

Avoid the need to synchronize

Since deadlocks are very hard to debug, one solution can be to design your service in a way where you don’t have to mutate the state in a threaded phase. One way to achieve this in Ruby is to use thread local variables. With the help of thread local variables you can save some state in a variable. After all your threads are joined you have access to them and can process them.

Here is an pseudo example:

Does this mean that thread local variables will be our saviors!?

No, they won’t. For one thing, sometimes it is simply not feasible or even possible to wait for the processing until every thread is finished.

And you have to be aware that a thread local variable is a form of global mutable state. If your thread doesn’t use to many objects/method calls and you never access this variable outside the thread block it can be possible to manage the danger associated with this. But if you design your app in a way where you change the thread state in many methods it will become very dangerous very quickly.

Synchronization strategies available in plain Ruby

Mutex

A mutex — short for mutual exclusion — can control the access to the resource. Here is the example with the DoorLock from above with a mutex:

Now, no matter how often we will call our class from various threads to unlock the door, it will be opened only once.

Mutexes don’t come for free. You have to be very careful to make sure that you don’t create a deadlock or a starvation — as shown in the first example.

But with the Mutex class you can implement some more sophisticated lock mechanism like e.g. readers-writers-locks and other synchronization methods. It is worth mentioning though that many of these methods are still prone to deadlocks, starvation or other problems associated with concurrent programming.

Conclusion

Multithreaded programs are hard — there are so many things to take care off! But if we want to squeeze every last bit of performance out of our CPUs we need to get to know the options and the risks associated with them.

Fortunately for us Ruby devs the future looks bright! TruffleRuby looks very promising and for CRuby there is so much development going into concurrency right now, it is hard to stay up to date. For everyone interested I can highly recommend they take a look into the GitHub mirror and the pull requests there.

[1] Although right now, it is not useful to use threads in Ruby for CPU bound operations it does make a lot of sense to use threads for IO bound operations. For an explanation look at this article.