Book Sneak Peak: Deadlocks

The following content is a small extract from my latest book Beyond Effective Go – Part 1 – Achieving High-Performance Code.

Deadlocks

Deadlocks occur when two or more goroutines are blocked waiting for resources held by each other. Putting this a different way, imagine two very stubborn children. Dinner time comes, and they both rush to the table. Each grabs one chopstick. Neither can eat with just one chopstick, but as long as neither is willing to give up the one they have, both will be prevented from eating. If we were to write this in code, we would have:

type GreedyChild struct {
	name string
}

func (g *GreedyChild) Eat(wg *sync.WaitGroup, chopsticks ...*chopstick) {
	defer wg.Done()

	for _, c := range chopsticks {
		c.Pickup()

		// interrupt scheduler (forcing the appearance of deadlock)
		runtime.Gosched()
	}

	g.eatUntilFull()

	for _, c := range chopsticks {
		c.PutDown()
	}
}

type chopstick struct {
	mutex *sync.Mutex
}

func (c *chopstick) Pickup() {
	c.mutex.Lock()
}

func (c *chopstick) PutDown() {
	c.mutex.Unlock()
}

Perhaps the most insidious aspect of this issue is that you can run this code, and sometimes it will work correctly. And yet other times, it will deadlock and panic with the error:  

fatal error: all goroutines are asleep - deadlock!

Deadlocks can also occur when using channels. The following code simulates a simple game of table tennis:

func main() {
	table := make(chan *ball)

	go newPlayer("paul: ping", table)
	go newPlayer("sally: pong", table)

	// throw a ball onto the table
	table <- &ball{}

	// throw a ball second ball. Causes deadlock
	table <- &ball{}

	<-time.After(3 * time.Second)
}

func newPlayer(action string, table chan *ball) {
	for thisBall := range table {
		// print action
		println(action)

		// send the ball back
		table <- thisBall
	}
}

Without the comments in the code above, it is challenging to see that a deadlock is possible. In fact, the code works perfectly until we add a second ball to the channel. Once that happens, everything gets stuck. The issue is that the write and read of the unbuffered channel are synchronized. This means that the program will eventually be in a state where both goroutines are trying to write to the channel simultaneously. Perhaps most insidiously, unlike our mutex example, Go’s tooling cannot detect this situation and running this code will panic.


If you have enjoyed this content and would like more, please pick up a copy of Beyond Effective Go – Part 1 – Achieving High-Performance Code, and don’t forget to also check out the book’s companion source code.

Also, if you would like to be notified when there are new posts or to be kept informed regarding the upcoming book launch please join my Google Group (very low traffic and no spam).