Skip to content

Day 064 — sync.Mutex & RWMutex

Month 3 · Week 2 · ⬅ Day 063 · Day 065 ➡ · Journal index

🎯 Learning Objective

Protect shared mutable state with sync.Mutex and, when reads dominate, sync.RWMutex, without introducing data races or deadlocks.

📚 Topics

  • sync.Mutex Lock/Unlock and the critical section
  • sync.RWMutex RLock/RUnlock vs Lock/Unlock
  • "Share memory by communicating" vs guarding memory with a lock

📖 Reading / Sources

📝 Notes

  • A [[data-race]] is two goroutines accessing the same memory concurrently with at least one write; the result is undefined behaviour, not just a wrong number. counter++ is load+add+store — three steps that interleave badly.
  • A [[mutex]] (sync.Mutex) makes a [[critical-section]] mutually exclusive: Lock() blocks until the lock is free, Unlock() releases it. Pair them with defer Unlock() immediately after Lock() so early returns and panics still release.
  • The zero value of a Mutex/RWMutex is an unlocked, ready-to-use lock — no constructor needed. Keep the lock next to the data it guards, often as an unexported struct field.
  • [[rwmutex]] (sync.RWMutex) allows many concurrent RLock holders or one Lock holder. Use it only when reads vastly outnumber writes; under write-heavy load it can be slower than a plain Mutex due to bookkeeping.
  • A Mutex is not reentrant: locking it twice in the same goroutine deadlocks. It is also not associated with a goroutine — any goroutine may Unlock, though idiomatically the locker unlocks.
  • Never copy a mutex after first use (it copies the lock state); go vet flags this. Pass *T where T embeds a mutex. This is the [[no-copy]] rule.
  • Channels coordinate ownership transfer; mutexes guard shared state in place. Rule of thumb: a channel for handing data between goroutines, a mutex for a struct field many goroutines read/write.

💻 Code Examples

type SafeCounter struct {
    mu sync.Mutex // guards n; lock lives next to the data it protects
    n  int
}

func (c *SafeCounter) Inc() {
    c.mu.Lock()
    defer c.mu.Unlock() // survives early return / panic
    c.n++
}

Full code: examples/month-03/mutex/main.go · Run: go run ./examples/month-03/mutex · Race check (after deleting the lock): go run -race ./examples/month-03/mutex

🏋️ Exercises / Practice

Exercise Status Link
Thread-safe counter (Mutex/RWMutex) exercises/month-03/week-2/safecounter

🐛 Mistakes Made

  • Returned early from a method before Unlock (no defer) → next Lock blocked forever. Moved to defer c.mu.Unlock().
  • Wrote c2 := *c to "snapshot" a counter and copied the mutex with it — go vet warned "assignment copies lock value". Passed *SafeCounter instead.

❓ Open Questions

  • When is RWMutex actually a win vs a plain Mutex? (Needs a benchmark with a realistic read/write ratio.)

🧠 Active Recall (answer without looking)

  1. Q: Why is counter++ unsafe across goroutines without a lock?
    A

It compiles to load→add→store; two goroutines can both load the same old value, so increments are lost. It is also a data race (undefined behaviour), not merely a lost update. 2. Q: What does go vet complain about if you pass a struct containing a sync.Mutex by value?

A

"assignment/argument copies lock value" — copying a mutex after use breaks mutual exclusion because the copy has independent lock state. Pass a pointer.

🪶 Feynman Reflection

A mutex is a single key to a room: only the goroutine holding the key may be inside the critical section, so the shared variable is never seen half-updated. An RWMutex is a library reading room — any number of readers at once, but the room is cleared for a single writer.

🕳️ Knowledge Gaps

  • Lock contention profiling (go test -bench + -mutexprofile) — revisit during the optimisation month.

✅ Summary

I can guard shared state with sync.Mutex (and RWMutex for read-heavy access), always defer Unlock, keep the lock beside its data, and avoid copying locks.

⏭️ Next Steps / Prep for Tomorrow

  • Day 065: sync.WaitGroup in depth — lock-free result collection and error aggregation without errgroup.

Time spent Difficulty Confidence
90 min 🟦🟦⬜⬜⬜ 🟦🟦🟦⬜⬜

Suggested commit: feat(examples): sync.Mutex and RWMutex safe counter (day 064)