Mutex
One wand, one stir at a time. Hand it on as you leave or the brew is undone.
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++
} 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
LockandUnlock. Anything not touching shared state should sit outside it for example long sleeps, network calls, etc. - Pair
Lockwithdefer Unlock.defer mu.Unlock()at the top of the critical section survives panics, early returns and your own forgetfulness. Exactly asdefer wg.Done()does for aWaitGroup. - Pass by pointer. A
Mutexmust not be copied after first use. Embed it in a struct and pass the struct as*Cauldron. Never asCauldron. - 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
Lockwith oneUnlockon every path. - Copying a
Mutexafter 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.