Concurrency in Go
For the past week or so, I’ve been pairing on an implementation of bitcoin (blog post all about that coming soon!). We ended up having multiple goroutines accessing the same data, which got me wondering about the most idiomatic way of handling concurrency in Go since it has both channels and traditional locks. These are a few things I learned about how to best manage concurrency in Go!
Goroutines
Goroutines are essentially lightweight threads. Since they run in the same address space, all access to shared memory must be synchronized.
The function passed to a goroutine will be run concurrently meaning synchronous functions can continue to run while the function in the goroutine runs separately in its own thread.
The following example will start running say("world")
in a goroutine while the main function executes say("hello")
synchronously.
This will produce output that looks something like this:
However, note that if the synchronous call following the goroutine takes less time to execute than the goroutine, the main
function will return without returning the result of the goroutine. Note how the following example does not print "world"
at all.
Basically by running a goroutine the main function is saying “Okay, run this saySlow function over here while I pay attention to the synchronous sayFast function.” The main function has already forgotten about asking the goroutine to handle the saySlow function, so it’s not waiting for any results from it. If sayFast
finishes executing before the goroutine gets a chance to execute, the main function will return anyway. This makes communication very important when executing things concurrently. And how do we communicate within goroutines? Channels!
Channels
Channels are conduits that connect concurrent goroutines. You put send values into channels from one goroutine and receive those values into another goroutine. This is the best way to pass resources from goroutine to goroutine.
In its simplest form, you can pass something onto a channel. And then read it somewhere else that also has access to that same channel.
Remember that problem we saw before where the synchronous function completed before the goroutine did? We don’t have to worry about that when we’re waiting to receive from a channel! By default, sends and receives block until both the sender and receiver are ready, so we don’t actually need to do anything else to synchronize our program!
By default, channels are unbuffered meaning they will only accept sends if there is a corresponding receive since there isn’t a place to stash any extra values. However, we can create buffered channels easily.
This will print out "Hey! Hey again!"
because we sent two messages to the channel and then pulled each message off of the channel.
Remember that example from earlier again? When the goroutine wasn’t even able to finish before the main function finished? We found that one way to solve it - waiting to receive a value from a channel. But what if we don’t actually need anything from the goroutine? What if it just prints some stuff? That’s okay! You can just pass a done
channel to the goroutine and let it tell you when it’s done with everything it needs to do.
What about waiting on multiple channel operations? You can use a select statement! Select statements block until one of its cases can run.
This example will output the following:
sync.Mutex
As I mentioned earlier, Go does allow for traditional locks. But when should you use them?
According to the Go wiki page, you can usually follow this general rule:
Channel: Use for passing ownership of data, distributing units of work, communicating async results Mutex: Use for caches and state
In the following example (mostly pulled from A Tour of Go), we make sure we can only increment SafeCounter
if no one else is using it meaning sync.Mutex is unlocked. Additionally, we can only view the value of the counter if it’s unlocked. This traditional locking ensures only one goroutine is messing with the counter at one time.
sync.WaitGroup
Lastly, we have WaitGroups, which are another primitive of the sync
package. They allow co-operating goroutines to collectively wait for an event before proceeding independently again.
The main goroutine calls Add
to set the number of goroutines to wait for. Then each of the goroutines runs and calls Done
when it’s finished. At the same time, Wait can be used to block until all goroutines have finished
Here is a silly example:
Conclusion
There are a few ways of handling concurrency in Go! Traditional locking should generally be used for simple stateful blocking while channels can be used for more complex communication and synchronization.
If you’re interested in reading more about concurrency, I highly recommend Rob Pike’s talk on Go Concurrency Patterns.