Concurrency Primitives

graph TD q1{"Reads vastly outnumber writes?"} -->|"yes"| rw["RWMutex"] q1 -->|"no"| q2{"Simple counter/CAS operation?"} q2 -->|"yes"| atomic["Atomic ops"] q2 -->|"no"| q3{"Limiting concurrency to N?"} q3 -->|"yes"| sem["Semaphore (buffered channel)"] q3 -->|"no"| mutex["Mutex"]

Mutex: protects a critical section. sync.Mutex in Go: spins briefly, then blocks on futex. Not reentrant — goroutine locking a mutex it already holds = deadlock.

RWMutex: multiple concurrent readers or one writer. Use when reads greatly outnumber writes (config cache, routing table).

Semaphore: integer counter limiting concurrency. In Go: make(chan struct{}, N) — send to acquire, receive to release. Models "at most N concurrent DB connections."

Spinlock: busy-wait. Avoids context switch. Only for very short critical sections (< 1µs). Wastes CPU on contention. Go runtime uses internally for scheduler locks.

Condition variable: wait until a condition is true while atomically releasing a mutex. Always loop: for !condition { cond.Wait() } to handle spurious wakeups.

Atomic operations: sync/atomic.CompareAndSwap — indivisible read-modify-write at hardware level. Lock-free, ~10× faster than mutex for simple operations. Used for counters, reference counting, lock-free queues.

Channel (Go): hchan with ring buffer + send/receive queues. Direct goroutine-to-goroutine transfer (zero-copy when both ready). Blocking goroutines are parked by the GMP scheduler, not spinning. Closing a channel broadcasts to all waiting receivers.

Worked example — picking the right primitive: a service has an in-memory cache read by 1,000 concurrent goroutines per second and updated by a background refresher once every 30 seconds, plus a hard rule that no more than 5 requests may be in flight to a rate-limited third-party API at once. Using a plain sync.Mutex around the cache would serialize all 1,000 reads-per-second through one lock, even though they're all just reading — one goroutine reading blocks 999 others that only wanted to read too. Switching to sync.RWMutex lets all 1,000 readers hold the read lock concurrently; only the once-every-30-seconds writer needs exclusive access, and it briefly blocks new readers only for the moment it takes to swap in the refreshed data. For the third-party API cap, a buffered channel of capacity 5 used as a semaphore (sem := make(chan struct{}, 5); acquire with sem <- struct{}{}, release with <-sem) caps concurrency directly — the 6th concurrent caller blocks on the send until one of the first 5 releases, with no busy-waiting and no risk of the count drifting the way a hand-rolled atomic counter could if a release were ever missed on an error path.