Very basic concurrency for beginners in Go

Computers can do things very quickly, and if you can make them do many things at the same time, jobs get finished even sooner. Modern computers have processors with many cores, and spreading load across those cores maximises performance, and therefore speed of execution. Coding such multi-threaded code was hard, until Go came along and ruined it by making it so easy.

In this article, we will explore:

The `go` keyword

A common gotcha when writing concurrent code

How to use the `sync.WaitGroup` to make sure our program doesn’t terminate prematurely

What kind of impact concurrent code can have on performance

How making code run concurrently introduces some unpredictability

The go keyword

To make a function run in the background, insert the keyword `go` before the call (like you do with `defer`).

So this:

func main(){ doSomething()}

Becomes:

func main(){ go doSomething()}

Now, the `doSomething` function will run in the background in a goroutine.

If you want to play along with this article but don’t have your Go environment setup yet, you can always use the Go playground over at http://play.golang.org.

Simple example

Let’s explore this in a little more detail, and look at some common gotchas that can trip us up.

I really love that adding concurrency in Go takes only three key presses, g, o and a space.

Running this code now (probably) produces the following output:

startend

Oh no — what happened? And why only “probably?”

In Go, when the main function exits, the program stops.

In our above code, the background task doesn’t get chance to write “do something,” before the program has ended — at which point, all goroutines are terminated.

To solve this, we could add a sleep operation at the bottom of our main function (with `time.Sleep`) but that’s not a very nice solution — because we don’t know how long our `doSomething` function might need to run.

Wait groups

The standard library gives us a package called `sync`, that has some great features which we can use to solve this problem properly.

A `sync.WaitGroup` is essentially a counter that we can increase (to indicate we want to wait for things), and decrease (to indicate things are done). Then we can tell code to wait until the WaitGroup counter reaches zero, which would mean all things have finished.

wg.Add(1) — indicates that there is 1 thing to wait for (our doSomething function)

wg.Wait() — indicates that code should block until the WaitGroup counter reaches zero

wg.Done() — indicates that 1 thing has finished

Notice that we don’t ever create a new WaitGroup, this is because in its default zero state (the state you get just by defining it), a WaitGroup is ready to use. Check out the sync.WaitGroup documentation for more about how to use them.

Now re-run your code, and you’ll (probably) see the following output:

startenddo something

What’s with all this “probably” talk? When we ask Go to run code concurrently, we can’t tell it exactly how and when to run the instructions. So it introduces some unpredictability in our code. As long as we understand this, it’s not a problem — and we can use things like WaitGroup objects to introduce synchronisation points between our goroutines.

If we move our wg.Wait() instruction to above the line where we write “end,” we can be sure that things happen in the order in which we intend. But then there wouldn’t be much point in running the code in the background, since we’d just be waiting in our ‘main goroutine’ anyway.

The `time.Sleep` instruction tells the code to block (or wait) for 100ms, this is to slow things down so we can really see the impact of making code run concurrently — you would probably never do this in real code.

Let’s enhance our main function to give us the times tables for all numbers that we needed to learn at school:

func main() { for n := 2; n <= 12; n++ { timestable(n) }}

Now, our code will call the timestable function for all numbers between 2 and 12 inclusively.

How long does it take?

On unix systems (including Macs), running a command prefixed by the `time` command will tell us how long our code took to execute. In my case it was just over 13 seconds:

$ time go run times.goreal 0m13.762suser 0m0.261ssys 0m0.059s

13 seconds to do this kind of work is embarassing if it weren’t for our time.Sleep instructions. But remember, it’s just to simulate some resource intensive task.

Multiplying concurrency

Now we are going to make the code run concurrently, to see if we can speed things up. Update the code:

Even though we are counting sensibly upwards, the order the operations actually occur varies. This is because each goroutine runs at its own pace, depending on things out of our control (like other things going on in the processor at the time.)

If we care about the order, then we’d need to use Go’s inbuilt Channels to send data safely across the goroutines. But that’s out of scope for this article.

Conclusion

We saw that making code run concurrently in Go is as easy as three key presses, and provided we are careful about how that behaves, we can get some pretty staggering results.