Go has native concurrency constructs which free the developer from dealing with OS threads and instead allows them to think in higher level abstractions. And it is elegant and refreshingly simple to use.
After safety and correctness, the main challenge with writing concurrent code, in general, is readability. Even with the constructs available in Go, it is quite easy to end up with code that requires the reader to jump around various sections of a function or file to make sense of the control flow.
Additionally with Go’s design of goroutines as entities without explicit cancellation and cooperative scheduling, it’s quite possible to introduce a bug that causes resource leaks by leaving behind goroutines that will never terminate.
Finally, error handling is always a challenge when using goroutines and often get’s ignored or simply logged and not propagated.
Is there a better way?
The inspiration for a solution came from reading Notes on structured concurrency, or: Go statement considered harmful. While I do not necessarily subscribe to everything in the post, the demonstrated nursery pattern did indicate an abstraction that could address the issues discussed above.
Implementing the pattern in Go is really easy and is available as part of my open source library: nursery: structured concurrency in Go. This not only implements the basic pattern but goes beyond in addressing error handling, context cancellations and running multiple copies of a given job. The rest of the post provides the highlights and use of this library.
The most frequently used API is RunConcurrently. It takes a list of jobs that need to be run concurrently with each other. The call to RunConcurrently does not exit unless all jobs have terminated, thus preventing the case of runaway goroutines.
The code is easier to read since we have a hierarchy of the concurrent jobs particularly when concurrent jobs might be nested within a concurrently running job (see code sample below).
The jobs which are nothing but wrapped goroutines canonically communicate via channels, so a typical structure could be the first job publishing into a channel and when done closing it; the second concurrent job reading from the channel and doing something with it.
This flow can be comprehended in a sequential manner. The expected outcome is a reduction of errors and improved readability.
Error Handling in concurrent code is generally a hard problem; and definitely harder than in sequential code. We’re doing two things here — each job is an anonymous function that takes a context and an error channel.
The responsibility of each job on encountering an error while doing its thing is to publish into the error channel. At this point, the job may choose to return from the function (ie. stop doing its thing) or continue processing.
Each job also receives a context — this allows for signaling a job doing expensive work to stop doing it if the context has been cancelled. The framework enables this by canceling the context on encountering the first error.
Once all concurrent jobs have ended the first error encountered is passed back to the caller and error handling is just like how you’d do it for a sequentially written program.
Sometimes we may want to run jobs that take a parent context to allow jobs to exit if the context is terminated. An example could be a context that has a timeout or a deadline. This is achieved with the RunConcurrentlyWithContext API.
RunMultipleCopiesConcurrently takes a copies count and a job and spawns that many concurrent jobs. This provides a very succinct way to structure concurrency for expensive jobs and is typically nested within a job running concurrently with a producer and consumer (see code sample below).
An involved scenario where we’re chaining 3 jobs (a producer, a worker, and a consumer). The worker being an expensive job itself, runs multiple copies concurrently.
An error handling scenario:
To close, the nursery library was simple and fun to write and personally it’s been extremely valuable in my projects. I believe it does solve some real problems and can make concurrency even easier.
I look forward to receiving feedback, comments, criticisms, pull requests but overall I hope you will find it equally valuable and make use of it.