Threads in one process share the same address space, so two threads can read and write the same bytes with no hardware barrier between them. A race condition is what happens when correctness depends on the timing of those accesses: each thread is correct in isolation, and the combination is wrong.
How two threads corrupt one balance
Take a bank account holding $100, with two threads each withdrawing $50:
// Thread 1: reads balance (100), computes 100 - 50 = 50
// Thread 2: reads balance (100), computes 100 - 50 = 50
// Thread 1: writes 50
// Thread 2: writes 50 <- correct answer is 0, not 50Both threads load the balance before either stores, so one withdrawal is silently lost. The defect is a lost update: a read-modify-write sequence that is not atomic, split by a thread switch in the middle. Any counter, queue length, or reference count updated this way drifts under load, which is why the pattern matters far beyond banking.
How a mutex serializes access to shared state
A mutex (mutual exclusion lock) marks a critical section — code that must run as if it were the only thread executing:
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&lock);
balance = balance - 50; // critical section: one thread at a time
pthread_mutex_unlock(&lock);A thread that arrives while the mutex is held blocks: the kernel parks it and schedules other work until the holder unlocks. An uncontended lock and unlock pair costs tens of nanoseconds through a fast user-space path (a futex on Linux); the expensive case is contention, where blocking costs a context switch of roughly 1–10 microseconds. For critical sections shorter than a switch, a spinlock skips parking and loops until the lock frees — cheaper when the wait is tens of cycles, wasteful when it is thousands.
How a semaphore limits N threads at once
A semaphore generalizes the mutex from one holder to N, guarding a pool such as four database connections. It keeps a counter with two operations:
sem_wait()decrements the counter and blocks while it would go below zerosem_post()increments the counter and wakes one waiter if any exist
A semaphore initialized to 1 behaves like a mutex. Initialized to N, it admits up to N threads and parks the rest. The failure mode is forgetting a sem_post on an error path, which permanently shrinks the pool — one leaked release per failure, until no thread can enter at all.
How condition variables wait for a state change
Locks serialize access; a condition variable lets a thread sleep until the shared state it needs actually arrives. A consumer calls pthread_cond_wait(), which releases the mutex and parks the thread in one atomic step; a producer that changes the state calls pthread_cond_signal() to wake one waiter, which re-acquires the mutex and re-checks its condition. The re-check is mandatory, not defensive: wakeups can be spurious, and another consumer may have taken the item first.
This lock-check-wait-signal-unlock cycle is the standard construction behind producer-consumer queues, readers-writers locks, and thread pools. Go exposes the same shape as sync.Cond, and every implementation shares the same rule: the condition is always state plus a lock, never a bare flag.
When locking turns into deadlock
Locking introduces its own failure: deadlock, where each thread waits for a lock another holds and none can proceed. It needs all four of the Coffman conditions at once — mutual exclusion, hold-and-wait, no preemption, and circular wait. In practice the cheapest defense is lock ordering: always acquire locks in the same global order, so the cycle can never form. Deadlock prevention in detail works through all four conditions and the strategies that break each one.
Production cost model and latency numbers
To apply Concurrency Basics: Locks and Semaphores effectively in production systems, engineers must move beyond qualitative diagrams and quantify the exact hardware and network costs at each boundary:
| Operation / Boundary | Typical Latency | Dominant Bottleneck | Production Mitigation |
|---|---|---|---|
| L1 / L2 Cache Access | 1 ns – 4 ns | Register dependency chains & pointer chasing | Contiguous memory layouts, cache-line alignment (64 B), and batch iteration |
| Main Memory (DRAM) Fetch | 60 ns – 100 ns | TLB misses, random heap indirection, NUMA hops | HugePages (2 MB), arena/slab allocators, and pre-allocated ring buffers |
| Kernel Syscall / Context Switch | 1 µs – 5 µs | User-to-kernel mode switch, page-table flush, lock contention | Non-blocking readiness loops (epoll / io_uring), batching, and zero-copy I/O |
| NVMe SSD Random Read / fsync | 50 µs – 500 µs | Block I/O queue depth, write amplification, WAL flushes | Group commit, sequential append logs, and bounded buffer-pool pinning |
| Intra-Region RPC Roundtrip | 0.5 ms – 2.5 ms | Serialization, TCP/TLS framing, tail queueing ($L = \lambda W$) | Connection pooling, deadline propagation, and p95 hedged requests |
Failure modes, debugging, and operational checklist
In real incidents, systems rarely fail at average (p50) load—they fail when a slow dependency, skewed hot key, or resource leak pushes concurrency across a non-linear saturation cliff. When reviewing code or architecture that relies on Concurrency Basics: Locks and Semaphores, walk through three verification questions:
- What is the invariant under partial failure or contention? If a worker crashes mid-operation, a network packet is delayed and reordered, or two threads race on the same state transition, prove which atomic primitive, version number, or idempotency key prevents silent corruption.
- How is backpressure enforced when demand exceeds capacity? Unbounded in-memory queues, unthrottled retries, and blocking calls without explicit deadlines turn a localized
100 msslowdown into a fleet-wide Out-Of-Memory (OOM) cascade. Always bound queue depths, propagate caller deadlines, and shed excess load early. - Which metrics and traces isolate the bottleneck in under 60 seconds? Instrument saturation, queue wait time vs service execution time, cache hit/miss ratios, and error rates by failure reason so on-call engineers can immediately distinguish a compute bottleneck from an I/O or lock-contention stall.