gonjure
A grimoire of go incantations

← back to the grimoire

Mutex

One wand, one stir at a time. Hand it on as you leave or the brew is undone.

grimoire fragment · main.go
package main

import (
	"fmt"
	"sync"
	"time"
)

// shared brew.
type Cauldron struct {
	mtx    sync.Mutex
	stirs int
}

func main() {
  var (
    cauldron = &Cauldron{}
    wg       sync.WaitGroup
  )

	for idx := 0; idx < 4; idx++ {
		wg.Add(1)

		go func() {
			defer wg.Done()

			cauldron.Stir(1500)
		}()
	}

	wg.Wait()
  
	fmt.Printf("brew complete %d stirs tallied\n", cauldron.stirs)
}

// only one apprentice may hold wand at a time.
func (cauldron *Cauldron) Stir(delay int) {
	cauldron.mtx.Lock()
	defer cauldron.mtx.Unlock()

	time.Sleep(time.Duration(delay) * time.Millisecond)

	cauldron.stirs++
}
The careful brewing
recipe ✦ Spell complete ✦ mu.Lock(), one apprentice at a time; the brew comes good
Dials & sigils

What you are witnessing

A sync.Mutex is a wand of stirring. It exists in one of two states: held or laid down on the bench. Lock() picks it up, if another goroutine already has it then the caller waits patiently and indefinitely until the wand is returned. Unlock() sets it back down, available for the next waiter to take. It is a stretch of work that prohibits multiple apprentices from doing simultaneously.

In the workshop, the apprentices each carry a single reagent and queue up to drop it into the cauldron. With the mutex on, the wand is solemnly passed around to ensure each reagent goes in order so that the brew completes as a shower of stars. Turn the toggle off and watch the same apprentices lunge at once, causing reagents to collide in mid air. The cauldron belches purple smoke - a textbook race condition.

Why a wizard cares

Concurrent code that touches shared state without coordination doesn't merely risk the wrong answer. It risks a different wrong answer on every run, often only in production and usually when under load. Go's race detector can catch many of these bugs but the surest defence is to declare what you protect and defend it the same way every time. A Mutex is the smallest yet strongest tool for this purpose.

Shape of the spell

  • Hold the wand only as long as you must. The critical section is the bit between Lock and Unlock. Anything not touching shared state should sit outside it for example long sleeps, network calls, etc.
  • Pair Lock with defer Unlock. defer mu.Unlock() at the top of the critical section survives panics, early returns and your own forgetfulness. Exactly as defer wg.Done() does for a WaitGroup.
  • Pass by pointer. A Mutex must not be copied after first use. Embed it in a struct and pass the struct as *Cauldron. Never as Cauldron.
  • Reads as well as writes. If one goroutine writes a field and another reads it, both sides need the lock. For workloads that are overwhelmingly read only, reach for sync.RWMutex.

Pitfalls

  • Deadlock. If two goroutines each hold the wand, forever awaiting for the other. Always acquire multiple locks in the same order, everywhere.
  • Unlocking an unlocked mutex will panic. Match every Lock with one Unlock on every path.
  • Copying a Mutex after use will result in disaster. The copy's state diverges from the original with one cauldron becoming two and neither is what you wanted.
  • A mutex is not a queue. Waiters are woken in an undefined order. It will not provide fairness or priority.
  • Holding the wand across a channel send or rpc is asking for trouble. The longer the critical section, the more other apprentices stand idle increasing the chance that a slow remote spirals into deadlock.

Reagents required: sync, already in your standard pouch.