We're sorry but this app doesn't work properly without JavaScript enabled. Please enable it to continue.

Mutexes in Go

Lane Wagner
Lane WagnerBoot.dev co-founder and backend engineer

Last published

Table of Contents

Golang is King when it comes to concurrency. No other language has so many tools right out of the box, and one of those tools is the standard library's sync.Mutex{}. Mutexes let us safely control access to data across multiple goroutines.

All the content from our Boot.dev courses are available for free here on the blog. This one is the "Mutexes" chapter of Learn Go. If you want to try the far more immersive version of the course, do check it out!

Mutexes in Go

Mutexes allow us to lock access to data. This ensures that we can control which goroutines can access certain data at which time.

Go's standard library provides a built-in implementation of a mutex with the sync.Mutex type and its two methods:

We can protect a block of code by surrounding it with a call to Lock and Unlock as shown on the protected() function below.

It's good practice to structure the protected code within a function so that defer can be used to ensure that we never forget to unlock the mutex.

func protected(){
    mu.Lock()
    defer mu.Unlock()
    // the rest of the function is protected
    // any other calls to `mu.Lock()` will block
}

Mutexes are powerful. Like most powerful things, they can also cause many bugs if used carelessly.

Maps Are Not Thread-Safe

Maps are not safe for concurrent use! If you have multiple goroutines accessing the same map, and at least one of them is writing to the map, you must lock your maps with a mutex.

Why Is It Called a “mutex”?

Mutex is short for mutual exclusion, and the conventional name for the data structure that provides it is "mutex", often abbreviated to "mu".

It's called "mutual exclusion" because a mutex excludes different threads (or goroutines) from accessing the same data at the same time.

The Concurrent Read/Write Problem

The principal problem that mutexes help us avoid is the concurrent read/write problem. This problem arises when one thread is writing to a variable while another thread is reading from that same variable at the same time.

When this happens, the reader could be reading bad data while it's being mutated in place. For maps, Go may detect the unsafe access and stop the program.

A Program Without a Mutex

package main

import (
	"fmt"
)

func main() {
	m := map[int]int{}
	go writeLoop(m)
	go readLoop(m)

	// stop program from exiting, must be killed
	block := make(chan struct{})
	<-block
}

func writeLoop(m map[int]int) {
	for {
		for i := 0; i < 100; i++ {
			m[i] = i
		}
	}
}

func readLoop(m map[int]int) {
	for {
		for k, v := range m {
			fmt.Println(k, "-", v)
		}
	}
}

The example above creates a map, then starts two goroutines which each have access to the map. One goroutine continuously mutates the values stored in the map, while the other prints the values it finds in the map.

If we run the program, one possible output is: fatal error: concurrent map iteration and map write

In Go, it isn't safe to read from and write to a map at the same time.

Mutexes to the Rescue

package main

import (
	"fmt"
	"sync"
)

func main() {
	m := map[int]int{}

	mu := &sync.Mutex{}

	go writeLoop(m, mu)
	go readLoop(m, mu)

	// stop program from exiting, must be killed
	block := make(chan struct{})
	<-block
}

func writeLoop(m map[int]int, mu *sync.Mutex) {
	for {
		for i := 0; i < 100; i++ {
			mu.Lock()
			m[i] = i
			mu.Unlock()
		}
	}
}

func readLoop(m map[int]int, mu *sync.Mutex) {
	for {
		mu.Lock()
		for k, v := range m {
			fmt.Println(k, "-", v)
		}
		mu.Unlock()
	}
}

In this example, we added a sync.Mutex{} and named it mu. In the write loop, the Lock() method is called before writing, and then the Unlock() is called when we're done. This Lock/Unlock sequence ensures that no other goroutines can Lock() the mutex while we have it locked; any other goroutines attempting to Lock() will block and wait until we Unlock().

In the reader, we Lock() before iterating over the map, and likewise Unlock() when we're done. Now the goroutines share the memory safely!

RW Mutex

The standard library also exposes a sync.RWMutex.

In addition to these methods:

The sync.RWMutex also has these methods for concurrent reads:

The sync.RWMutex can improve performance in read-intensive processes. Multiple goroutines can safely read from the map simultaneously, as many RLock() calls can occur at the same time. However, only one goroutine can hold a Lock(), and during this time, all RLock() operations are blocked.

When to Use a Read/Write Mutex

To be precise, maps are safe for concurrent read access, just not concurrent read/write or write/write access. A read/write mutex allows all the readers to access the map at the same time, but a writer will still lock out all other readers and writers.

package main

import (
	"fmt"
	"sync"
)

func main() {
	m := map[int]int{}

	mu := &sync.RWMutex{}

	go writeLoop(m, mu)
	go readLoop(m, mu)
	go readLoop(m, mu)
	go readLoop(m, mu)
	go readLoop(m, mu)

	// stop program from exiting, must be killed
	block := make(chan struct{})
	<-block
}

func writeLoop(m map[int]int, mu *sync.RWMutex) {
	for {
		for i := 0; i < 100; i++ {
			mu.Lock()
			m[i] = i
			mu.Unlock()
		}
	}
}

func readLoop(m map[int]int, mu *sync.RWMutex) {
	for {
		mu.RLock()
		for k, v := range m {
			fmt.Println(k, "-", v)
		}
		mu.RUnlock()
	}
}

By using a sync.RWMutex, our program can become more efficient. We can have as many readLoop() goroutines as we want, while still ensuring that the writers have exclusive access.

Frequently Asked Questions

What does mutex stand for in Go?

Mutex is short for mutual exclusion, and the conventional name for a mutex variable is mu.

What methods does sync.Mutex provide?

sync.Mutex provides Lock and Unlock. Other goroutines that call Lock while the mutex is locked will block until it is unlocked.

Why should Mutex.Unlock be deferred?

Structuring protected code within a function lets you defer Unlock so you do not forget to unlock the mutex.

Are Go maps safe for concurrent use?

Maps are safe for concurrent reads, but concurrent read/write or write/write access must be protected.

What does sync.RWMutex do?

RWMutex allows multiple goroutines to hold read locks at the same time, while a write lock excludes all other readers and writers.