Step 1 of 6 · Reading · ~4 min
Learn
Concurrency Patterns
sync.Mutex, RWMutex and Once
By the end of this lesson you can share one structure between goroutines safely, let many readers share a lock, and run one-time setup exactly once.
In Goroutines, eight workers incrementing one counter lost updates, and you avoided it by not sharing. This lesson is the safe way to share.
Mutex: one goroutine at a time
A mutex (mutual exclusion lock) lets one goroutine at a time into a stretch of code, the critical section. Lock blocks until the lock is free; Unlock releases it, and defer right after Lock covers every return path. Keep the mutex in the struct beside the data it guards. A ticket shop's check and update must be one step:
Unlock between the check and the update and two buyers can both take the last ticket, leaving -1. With 1500 goroutines each calling s.Take("seat", 1) on a stock of 1000, the lock gives exactly 1000 true results and a final stock of 0. Without it the count may be wrong, or the runtime may abort with fatal error: concurrent map writes. That is a fatal error, not a panic, so recover cannot catch it, and a handful of calls can pass without the lock by luck.
Two rules about the mutex itself. Never copy a mutex: A copy is a different lock. With a value receiver, func (s Stock) Take(...), every call locks its own private copy and excludes nobody, while the map still points at the one shared map. The built-in static checker, go vet, reports this as "passes lock by value".
Keep the locked section short, and keep the mutex next to the data it guards — an unexported field beside the map, not an embedded sync.Mutex, which would publish Lock and Unlock as methods of your own type. The starter already drives the goroutines with a sync.WaitGroup from the goroutines lesson: Add before go, one Done per goroutine, and Wait before reading the result. The WaitGroup says when the work finished; it never makes a shared map safe to write.
RWMutex: many readers or one writer
sync.RWMutex adds a shared mode: any number of goroutines may hold RLock at once, or exactly one may hold Lock, never both.
A Set would take Lock. It pays off only when reads greatly outnumber writes. Two traps: a write under RLock is still a race, because readers share the lock; and a goroutine holding RLock that then calls Lock waits for itself forever. go run -race main.go prints WARNING: DATA RACE with the call stacks of colliding accesses, even for races that gave the right answer by luck; this grader runs plain go run, so use it locally.
Once: setup that runs exactly once
sync.Once.Do(f) runs f the first time it is called; every other caller, even one arriving while f is running, waits until f returns. A Once must not be copied either. A config parsed on first use:
Testing if c.vals == nil instead is racy: two goroutines can both see nil and parse twice, and a third may read a half-built map.
Two deadlocks
A mutex is not reentrant: the goroutine holding it cannot lock it again. If Stock.Summary holds s.mu and calls a method that also begins with s.mu.Lock(), it waits for itself. Go prints fatal error: all goroutines are asleep - deadlock! if every goroutine is blocked; otherwise it just hangs. Fix it with a lowercase helper that assumes the lock is held.
The second comes from lock order, the sequence in which a goroutine takes several locks. If transfer(a, b) locks a then b while transfer(b, a) locks b then a, each can hold one lock and wait for the other. Fix it with one global order, such as the lower id first.
Use a channel when ownership of a value moves between goroutines; a mutex when several update one structure in place.
Common mistakes
- A value receiver on a type holding a mutex. Large hidden inputs crash with
concurrent map writes; small ones pass. - A nil check instead of
Once. The build counter reads more than 1. - Locking inside a locked method. The program hangs.
Your exercise
Count words across lines of text with one goroutine per line, into a shared Counter guarded by its mutex, with the stop-word set built lazily through a sync.Once. The starter handles the input and output. The most likely mistake is a value receiver on Add.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…