Multithreaded Ruby — Synchronization, Race Conditions and Deadlocks

Photo by yip vick on UnsplashMultithreaded Ruby — Synchronization, Race Conditions and DeadlocksThreaded programming is hard.

Mysterious non-deterministic bugs and other issues can haunt you.

Let’s see what we can do about this.

Gernot GradwohlBlockedUnblockFollowFollowingJun 19Threads, even on MRI Ruby, can lead to race conditions and deadlocks which can crash your service and are very hard to identify because of the non-deterministic nature.

So, let’s take a look into multithreaded synchronization and the evil that comes 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 PlaceIn one word: 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 loose 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 ImplementationsJRubyWith 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.

TruffleRubyTruffleRuby 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 Happen Only in a Memory Shared Multithreaded Model?Recently, there has been quite a fuzz around new languages that implement concurrency in different ways.

Elixir, Go, Closure, Kotlin and Scala with the Akka framework are only a few gems that appeared during this time.

And although it is true that concurrency is easier without threads but different approaches won’t solve everything.

Here is a blog post that describes a deadlock in a CSP based concurrency approach.

How to Avoid DeadlocksDisclaimer: 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 SynchronizeSince 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 and 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 much objects/method calls and you never access this variable outside of 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 soon.

Synchronization Strategies Available in Plain RubyMutexA 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.

But 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.

ConclusionMultithreaded 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 CPU’s we need to get to know the options and also the risks associated with them.

But 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 kind of hard to stay up to date.

For everyone interested I can highly recommend to 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 into this article.

.

. More details

Leave a Reply