Understanding Mutexes

4th October 2013

For anyone new to building web applications with Go, it's important to realise that all incoming HTTP requests are served in their own Goroutine. This means that any code in or called by your application handlers will be running concurrently, and there is a risk of race conditions occurring.

In case you're new to concurrent programming, I'll quickly explain the problem.

Race conditions occur when two or more Goroutines try to use a piece of shared data at the same time, but the result of their operations is dependent on the exact order that the scheduler executes their instructions.

As an illustration, here's an example where two Goroutines try to add money to a shared bank balance at the same time:

InstructionGoroutine 1Goroutine 2Bank Balance
1Read balance ⇐ £50£50
2Read balance ⇐ £50£50
3Add £100 to balance£50
4Add £50 to balance£50
5Write balance ⇒ £150£150
6Write balance ⇒ £100£100

Despite making two separate deposits, only the second one is reflected in the final balance because the two Goroutines were racing each other to make the change.

The Go blog describes the downsides:

Race conditions are among the most insidious and elusive programming errors. They typically cause erratic and mysterious failures, often long after the code has been deployed to production. While Go's concurrency mechanisms make it easy to write clean concurrent code, they don't prevent race conditions. Care, diligence, and testing are required.

Go provides a number of tools to help us avoid data races. These include Channels for communicating data between Goroutines, a Race Detector for monitoring unsynchronized access to memory at runtime, and a variety of 'locking' features in the Atomic and Sync packages. One of these features are Mutual Exclusion locks, or mutexes, which we'll be looking at in the rest of this post.

Creating a Basic Mutex

Let's create some toy code to mimic the bank balance example:

import "strconv"

var Balance = &currency{50.00, "GBP"}

type currency struct {
  amount float64
  code   string
}

func (c *currency) Add(i float64) {
  // This is racy
  c.amount += i
}

func (c *currency) Display() string {
  // This is racy
  return strconv.FormatFloat(c.amount, 'f', 2, 64) + " " + c.code
}

We know that if there are multiple Goroutines using this code and calling Balance.Add() and Balance.Display(), then at some point a race condition is likely to occur.

One way we could prevent a data race is to ensure that if one Goroutine is using the Balance variable, then all other Goroutines are prevented (or mutually excluded) from using it at the same time.

We can do this by creating a Mutex and setting a lock around particular lines of code with it. While one Goroutine holds the lock, all other Goroutines are prevented from executing any lines of code protected by the same mutex, and are forced to wait until the lock is yielded before they can proceed.

In practice, it's more simple than it sounds:

import (
  "strconv"
  "sync"
)

var mu = &sync.Mutex{}
var Balance = ¤cy{50.00, "GBP"}

type currency struct {
  amount float64
  code   string
}

func (c *currency) Add(i float64) {
  mu.Lock()
  c.amount += i
  mu.Unlock()
}

func (c *currency) Display() string {
  mu.Lock()
  amt := c.amount
  cur := c.code
  mu.Unlock()
  return strconv.FormatFloat(amt, 'f', 2, 64) + " " + cur
}

Here we've created a new mutex and assigned it to mu. We then use mu.Lock() to create a lock immediately before both racy parts of the code, and mu.Unlock() to yield the lock immediately after.

There's a couple of things to note:

Let's tidy up the example a bit:

import (
  "strconv"
  "sync"
)

var Balance = &currency{amount: 50.00, code: "GBP"}

type currency struct {
  sync.Mutex
  amount float64
  code   string
}

func (c *currency) Add(i float64) {
  c.Lock()
  c.amount += i
  c.Unlock()
}

func (c *currency) Display() string {
  c.Lock()
  defer c.Unlock()
  return strconv.FormatFloat(c.amount, 'f', 2, 64) + " " + c.code
}

So what's changed here?

Because our mutex is only being used in the context of a currency object, it makes sense to anonymously embed it in the currency struct (an idea borrowed from Andrew Gerrard's excellent 10 things you (probably) don't know about Go slideshow). If you look at a larger codebase with lots of mutexes, like Go's HTTP Server, you can see how this approach helps to keep locking rules nice and clear.

We've also made use of the defer statement, which ensures that the mutex gets unlocked immediately before the function executing it returns. This is common practice for functions that contain multiple return statements, or where the return statement itself is racy like in our example.

Read Write Mutexes

In our bank balance example, having a full mutex lock on the Display() function isn't strictly necessary. It would be OK for us to have multiple reads of Balance happening at the same time, so long as nothing is being written.

We can achieve this using RWMutex, a reader/writer mutual exclusion lock which allows any number of readers to hold the lock or one writer. Depending on the nature of your application and ratio of reads to writes, this may be more efficient than using a full mutex.

Reader locks can be opened and closed with RLock() and RUnlock() like so:

import (
  "strconv"
  "sync"
)

var Balance = &currency{amount: 50.00, code: "GBP"}

type currency struct {
  sync.RWMutex
  amount float64
  code   string
}

func (c *currency) Add(i float64) {
  c.Lock()
  c.amount += i
  c.Unlock()
}

func (c *currency) Display() string {
  c.RLock()
  defer c.RUnlock()
  return strconv.FormatFloat(c.amount, 'f', 2, 64) + " " + c.code
}

If you found this post useful, you might like to subscribe to my RSS feed.

Filed under: golang, tutorial
Last updated: 16th July 2014 (for Go version 1.3)

‚Äč