Mutex and Semaphore Runtime
Mutex and Semaphore Runtime
Overview
sync.Mutex is not a pure userspace spin forever. After a short path, contending lockers park through runtime semaphores (sudog queues), integrating with the scheduler like channel waits.
Diagram: Lock fast/slow path
Lock attempt
│
├── uncontended CAS ──► enter critical section
│
└── contended ──► park (semacquire)
│
Unlock ──► wake one waiter ──► runnable
Fast Path / Slow Path
Lock attempt
-> atomic CAS (uncontended) success → return
-> spin briefly (optional, implementation)
-> enter runtime semacquire queue
-> G parks
Unlock
-> hand off / wake waiter
-> woken G runnable
Implications:
- Uncontended locks are extremely cheap
- High contention shows up in block/mutex profiles, not only CPU
- Holding locks across I/O serializes your service (top production mistake)
RWMutex
Multiple readers; writers exclusive. Writer starvation possible under continuous readers — measure.
Cond
sync.Cond builds on mutex + notification; easy to misuse. Prefer channels for many app-level designs; Cond appears in specialized queues.
Atomics vs Mutex
| Need | Prefer |
|---|---|
| Counter / flag | atomic |
| Multi-field invariant | Mutex |
| Coordination / ownership transfer | Channel |
Avoid
mu.Lock()
resp, err := http.Get(url) // BAD: lock held across I/O
mu.Unlock()Experiment
go test -bench=. -benchmempackage lock_test
import (
"sync"
"sync/atomic"
"testing"
)
func BenchmarkMutex(b *testing.B) {
var mu sync.Mutex
var x int
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
mu.Lock()
x++
mu.Unlock()
}
})
}
func BenchmarkAtomic(b *testing.B) {
var x atomic.Int64
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
x.Add(1)
}
})
}What to notice: Atomics win for simple counters; mutexes win for complex critical sections — profile contention, don’t guess.
Try next: Enable mutex profile on a service and find the top Unlock edge under load.