September 26, 2017

As a new coder, I often find documentation less than helpful. Or at least, very difficult to parse. Recently I ran into this while reading the documentation for the concurrent-ruby gem. I want to be clear, the documentation for this gem is not bad. On the contrary, it is dense and thorough, which can nevertheless be detrimental for looking up a simple use case. I had trouble stripping it down to what I needed, so I'm presenting my results here as a reference for others.

CRuby is not known for being good at concurrency within a single process (such as a single web request). You can create new threads within a process, but those threads will execute one at a time due to a Global Interpreter Lock. That means that you can't really execute two instructions at once, but you can start multiple operations and Ruby will switch between them as resources allow. In practice, this is most often useful when making calls to external services with significant latency. You can start an i/o operation, then execute some code while waiting for the result.

The previous paragraph is what I knew from various internet readings. I didn't know how to actually do it in code, though. For a long time I didn't need to.

Our business uses third-party scheduling software, and I needed to make some API calls to this software for the app that I'm building. These API calls are slow (several seconds apiece), so I wanted to make them concurrently rather than sequentially. This is a textbook case for concurrency in CRuby, since the majority of time is spent waiting on i/o rather than code execution.

Here's what I wanted to do concurrently:

var_1 = some_api_call_result
var_2 = some_other_api_call_result
# some code that uses var_1 and var_2

And here's how I did it in the end:

var_1 = Concurrent::Future.execute { some_api_call }
var_2 = Concurrent::Future.execute { some_other_api_call }
var_1 = var_1.value
var_2 = var_2.value
# some code that uses var_1 and var_2

The execute method tells the interpreter to run the API call and continue program execution while waiting for it to complete. The value method returns the result of the code block passed to the execute method, blocking program execution if necessary while the operation completes, ensuring that the values are present for executing the code that follows.

Using this pattern, i/o operations can be run asynchronously by creating Concurrent::Future (docs) objects and passing the required code as a block to the execute method. There is no need to block program execution until the result of the i/o operation is needed by the program, at which point the value method is used to obtain it.

This seems to me like the most basic form of concurrency possible within a Ruby program, yet I had trouble finding it presented in a really simple way. I hope it helps someone else dealing with concurrency for the first time, and I encourage others to post their stories of basic operations that were difficult to learn.

Edit March 2, 2018: Chris Frank replied to this post with an easy way of accomplishing the same thing using only the Ruby standard library:

call_1 = Thread.new { some_api_call }
call_2 = Thread.new { some_other_api_call }

response_1 = call_1.value
response_2 = call_2.value