DEV Community

Brian Kephart
Brian Kephart

Posted on • Originally published at briankephart.com on

Basic Concurrency in Ruby. Really Basic.

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
Enter fullscreen mode Exit fullscreen mode

And here's how I did it in the end:
[note: See the comments for how to do this without the concurrent-ruby gem, using the Ruby standard library!]

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
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (2)

Collapse
chrisfrank profile image
chrisfrank • Edited on

Brian,

Thanks for taking the time to bring clear documentation into the ruby community. For writing complex concurrent code, concurrent-ruby is an excellent library--but for simple concurrency like API calls, it may be overkill. Ruby actually has solid concurrency built into its standard library, via its Thread class.

To make multiple API calls simultaneously with threads, following your example above, you'd do something like this:

call_1 = Thread.new { some_api_all }
call_2 = Thread.new { some_other_api_all }

response_1 = call_1.value
response_2 = call_2.value

Here's the same idea, with less repetition:

data = ["https://some_api",  "https://another_api"].map { |url|
  Thread.new { SomeHttpLibrary.get(url) }
}.map { |thread| thread.value }
# data is an array of API responses [response1, response2]

Check this blog post from Thoughtbot for more on how threads work. I have no affiliation with Thoughtbot, I just think their blog is a great resource for rubyists of all experience levels.

Collapse
briankephart profile image
Brian Kephart Author

Thanks for the reply, and for adding another (better!) approach.

I used concurrent-ruby because it was already in my project as a Rails 5 dependency, so when I started my search it seemed like the obvious tool for the job. Now it turns out what I needed was in the standard library all along, go figure.