Concurrency with Actors, Goroutines & Ruby

The world of concurrent computation is a complicated one. We have to think about the hardware, the runtime, and even choose between half a dozen different models and primitives: fork/wait, threads, shared memory, message passing, semaphores, and transactions just to name a few. Hence, not surprisingly, when Bruce Tate asked Matz, in an interview for his recent book (Seven Languages in Seven Weeks) for a feature that he would like to change in Ruby if he could go back in time, the answer was telling: “I would remove the thread and add actors or some other more advanced concurrency features”.

Process Calculi & Advanced Concurrency

It is easy to read a lot into Matz's statement, but the follow-up question is: more advanced concurrency features? Thousands of programmers are graduating each year after being taught about threads, mutexes and semaphores – that is how we do concurrency! Is there a crucial lecture we all missed on the “advanced concurrency” topic?

The answer is, probably not, to a large extent due to the "success" of the shared-memory model. Process calculi is the formal name for the study of many related approaches of modeling the behavior of concurrent systems, which provides many alternatives: CCS, CSP, ACP, and Actor models just to name a few. However, few of those acronyms have found their way into our lexicon, which is somewhat surprising given that most of them have their roots dating back to the early 1970's - hardly new stuff, but also rarely mentioned until recently.

Actors, CSP and Pi-calculus

The actor concurrency model, which is now gaining traction thanks to the recent success of the languages such as Erlang) and Scala) is a great example of an “alternative concurrency model” that is worth exploring. However, it is also not the only one. Google’s Go also brought back a related, but also a very different model: a mix of Tony Hoare's CSP and pi-calculus. On the surface, all very different languages, but also all based around a common core principle:

“Don’t communicate by sharing state; share state by communicating”

Let that sink in for a minute. Instead of protecting our data structures by locks, and then contending to acquire the lock, this model encourages us to explicitly pass state from process to process. This guarantees that only one process can have access to data at a given time and immediately eliminates an entire class of concurrency problems.

Actors, Goroutines, Channels & Ruby

So, given the similarity, what are the actual differences between languages such as Erlang and Go? Syntax and VM implementation details aside, in Erlang we communicate by giving each individual process a name - think mailbox#Concurrency_and_distribution_orientation). By contrast, in Go, all processes are anonymous and we name the "communication channel" instead - think UNIX pipes. A subtle difference, but one that leads to very different programming models and capabilities.

With some theory out of the way, let's take a closer look at Go's concurrency model. Install the Go runtime, or as it turns out, we can also emulate much of its model directly on top of our Ruby runtime (gem install agent):

c = Agent::Channel.new(name: 'incr', type: Integer)

go(c) do |c, i=0|
  loop { c << i+= 1 }
end

p c.receive # => 1
p c.receive # => 2
Agent - Go-like concurrency, in Ruby

A simple generator example, but one that highlights many different features. First, we create a named (‘incr’), typed channel, over which our processes will communicate. Then, the interesting bit: we call the “go” function, which takes a channel and a Ruby codeblock.

Under the hood, our “go” function actually spawns a new thread to execute our code block (the loop statement) and returns immediately to call receive on the original channel. The channel, in turn, blocks on receive until the producer has generated a value! In other words, we have just written a multi-threaded producer-consumer, except that there is not a single thread or a mutex in sight! Also take a look at a more interesting multi-worker example, sieve of Eratosthenes, and the results of a non-scientific shootout between MRI and JRuby.

Adopting "Advanced Concurrency"

Shared memory, threads and locks have their place and purpose. In fact, if you look under the hood of Agent, or within the source of any runtime with an “alternative concurrency model”, you will undoubtedly find them there at work. So, the question is not whether threads need to exist, but rather, whether they actually make for the best high-level interface to write, test, and manage code that requires concurrency, regardless of runtime.

Actors, and CSP/pi-calculus models may appear complicated at first sight, but mostly so because they are unfamiliar. In fact, they are all remarkably simple, powerful, and reproducible within any runtime once you have a few examples under your belt. Boot into Erlang, give Go a try, or install Agent to prototype some ideas with Ruby, it will be time well spent.

Ilya GrigorikIlya Grigorik is a web ecosystem engineer, author of High Performance Browser Networking (O'Reilly), and Principal Engineer at Shopify — follow on Twitter.