Home

Well behaved goroutines

Tuesday 28 May 2013

Making sure your goroutines play nicely is straightfoward if you know how.

Children

Imagine you (Mum) have two small children, Alice and Bob. They keep talking at the same time and you can't hear what's going on. You'd like it if only one of them spoke at once.

You give Alice and Bob each a ball and you make a rule that you can only speak if your ball is in the cup, which you place in front of them. There's only room in the cup for one ball. You imagine them taking turns to place their ball in the cup, speaking, then taking their ball out.

Oh how wrong you were! Alice and Bob seem to completely ignore the rule, speaking whenever they feel like. There's still only one ball in the cup at a time, but the children don't seem to understand!

So you try a different approach. You create a talking stick , and impose the rule that you can only speak if you're holding it. And only one person at a time can hold it, after which you put it in the middle.

This works well! You can now what they both have to say!

Goroutines

So in what weird way does this analogy relate to Go? Alice and Bob are goroutines (A and B), and Mum is the main goroutine.

In the first instance, we represent the cup by a buffered channel with capacity 1, initially empty. A and B send their ball (string) on it, speak (access shared memory), and receive their ball from it (run in playground):

var shared string
var cup = make(chan string, 1)

func child(name string) {
	ball := name + "'s ball"
	for {
		cup <- ball
		shared = name
		fmt.Println(shared)
		<-cup
	}
}

func main() {
	go child("Alice")
	go child("Bob")
}
But the write to shared is not thread-safe, as reported by Go's race detector:
WARNING: DATA RACE
Write by goroutine 5:
  main.child()
      balls.go:15 +0xc4

Previous write by goroutine 4:
  main.child()
      balls.go:15 +0xc4

The second idea represents each goroutine receiving the stick from a global channel, and then modifying the global state, before putting the stick back on the floor. This runs (playground) without data races:

var shared string
var floor = make(chan string, 1)

func child(name string) {
	for {
		stick := <-floor
		shared = name
		fmt.Println(shared)
		floor <- stick
	}
}

func main() {
	stick := "the talking stick"
	floor <- stick
	go child("Alice")
	go child("Bob")
}

Why?

Go's memory model is pretty simple - but there is a small subtlety concerning buffered channels. Unbuffered channels synchronise the send and receive operations so that the start of the send happens before the receive, which happens before the send completes. Buffered channels only have the guarantee that each send happens before the corresponding receive.

So the compiler can, if it chooses, re-order instructions that appear after the send so that they execute before the send (hoisting). It can also delay instructions that appear before the receive til after the receive (sinking).

So?

A race condition can trigger undefined behaviour. That's the full-blown C kind. Technically, anything can happen. This could occur, for example, if you're trying to use channels to simulate a mutex.

But these situations are unlikely to occur if you don't overcomplicate things. Go's motto is "share memory by communicating", and in these examples we're using shared global variables. We're asking for trouble! The proper way of synchronising these goroutines is to use a (possibly buffered) channel to pass what you want to say to another goroutine that prints them in order. Or if you insist, just use a real sync.Mutex to protect your global variables. In the child analogy, you'd ask them to write down what they want to say so you can peruse at your leisure!

Go provides the ability to write multi-threaded programs easily. But it will still allow you to make mistakes if you try to be too clever. But even if you do, the race detector provides fantastic support for tracking them down. As Brian Kernighan said:

Everyone knows that debugging is twice as hard as writing a program in the first place. So if you're as clever as you can be when you write it, how will you ever debug it?


Previous (Codejam 2013 Score Distribution)
Next (Go's range clause)