Why can this code deadlock when A holds an RLock and calls B, which takes the write Lock?
A takes the read lock and calls B, which takes the write lock on the same RWMutex. Under load this can deadlock.
var mu sync.RWMutex
func A() {
mu.RLock()
defer mu.RUnlock()
B()
}
func B() {
mu.Lock()
defer mu.Unlock()
}
Identify the bug and explain the cause.
Go's RWMutex is not reentrant. A holds a read lock and calls B, which requests the write Lock. If another goroutine is already waiting for Lock, the runtime blocks new readers to avoid writer starvation — so B waits on a read lock that A still holds. The docs forbid recursive read locking; fix by locking once at the top level.
- ✗Assuming
sync.RWMutexis reentrant — acquiring it recursively can deadlock - ✗Thinking the deadlock is unconditional rather than triggered by a concurrent waiting writer
- ✗Taking a lock inside a nested call instead of once at the top of the call chain
- →Why does the RWMutex block new readers once a writer is waiting?
- →How would you restructure
AandBso only one of them takes the lock?
Find the bug
var mu sync.RWMutex
func A() {
mu.RLock()
defer mu.RUnlock()
B()
}
func B() {
mu.Lock() // wants the write lock
defer mu.Unlock()
}
Why it deadlocks under load
sync.RWMutex is not reentrant: a goroutine that holds the lock cannot take it again.
A takes RLock and calls B, which requests the write Lock. One such stack alone might pass, but the trap is the anti-writer-starvation policy:
If at least one goroutine is already waiting on
Lock(), subsequentRLock()calls block, so the waiting writer eventually gets access.
Under contention this is fatal: while another goroutine waits for Lock, B also blocks on Lock, but cannot get it while A holds RLock — and A will not release RLock until B returns. The cycle is closed.
⚠️ The Go docs explicitly forbid recursive read locking. The fix is to not lock inside the nested call; take the lock once at the top level:
func A() {
mu.Lock()
defer mu.Unlock()
bLocked() // a version of B with no lock of its own
}