Working with concurrency is all about organization.
If you want to take advantage of 100% of what your system offers, you need concurrency. If you have a single-threaded program, and aren’t using some other form of concurrency (like processes), then you’re only using a fraction of what your system offers.
Our job is to organize our code such that the system can run it in the most efficient way possible. Introducing multiple threads is one way to do this. But then we need to make sure that our code is organized to preserve thread safety.
Working with concurrency is about balancing these elements of organization: organizing our code so that it can take maximum advantage of system resources, while still preserving the underlying data.
Part of the point I’m making is that concurrency isn’t something that should be used everywhere, not in every program, and not in every part of your application. Part of organizing our code is to decide where to apply concurrency, and where to restrict it.
The trick with multi-threaded programming is to strike the balance between firing on all cylinders all of the time, and utilizing more than a fraction of what your system has to offer.
Both of these extremes can be painful, so we should seek the place in the middle. The most succint set of rules I’ve seen to find this middle ground are laid out eloquently on the JRuby wiki.
The safest path to concurrency:
- Don’t do it.
- If you must do it, don’t share data across threads.
- If you must share data across threads, don’t share mutable data.
- If you must share mutable data across threads, synchronize access to that data.
If you stick to these rules, you’ll strike that balance.
Ruby concurrency doesn’t suck
I think there’s a general vibe in the programming community that Ruby isn’t a suitable environment for concurrency. I’ll certainly agree that this was the case in the past.
When MRI was the only Ruby implementation, using green threads instead of native threads along with a GIL, Ruby did not have a good story when it came to concurrency; the only option was to start more processes. Indeed, that’s still the approach favoured by a lot of Ruby applications to this day, and it does work.
But Ruby’s concurrency story is much better today. Threading on all of the major Ruby implementations is backed by native threads. While MRI still has a GIL, there are alternatives (JRuby and Rubinius) that support true parallel threading.
The Ruby language still doesn’t ship with much support for multi-threaded concurrency, but there are a growing number of options available in the community. Celluloid is a great example of this. It simplifies concurrent programming, while really embracing the constructs of the language. Writing programs using
Mutex#synchronize can be very challenging and lead to poorly factored Ruby code. The same program written using Celluloid can provide the same concurrency, but feel more like idiomatic Ruby.
As a language, Ruby could provide better primitives to support multi-threaded concurrency. But even today, Ruby doesn’t suck for concurrency. And I think Ruby’s concurrent future looks even brighter.