TRT
Sign in
Operating SystemsUpdated

Concurrency Basics: Locks and Semaphores

How threads share memory safely using mutexes, semaphores, condition variables, and the race conditions they prevent.

The Runtime Theory Team8 min read#concurrency#locks#semaphores#race-conditions#threads

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:

c
// 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 50

Both 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:

c
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 zero
  • sem_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 / BoundaryTypical LatencyDominant BottleneckProduction Mitigation
L1 / L2 Cache Access1 ns – 4 nsRegister dependency chains & pointer chasingContiguous memory layouts, cache-line alignment (64 B), and batch iteration
Main Memory (DRAM) Fetch60 ns – 100 nsTLB misses, random heap indirection, NUMA hopsHugePages (2 MB), arena/slab allocators, and pre-allocated ring buffers
Kernel Syscall / Context Switch1 µs – 5 µsUser-to-kernel mode switch, page-table flush, lock contentionNon-blocking readiness loops (epoll / io_uring), batching, and zero-copy I/O
NVMe SSD Random Read / fsync50 µs – 500 µsBlock I/O queue depth, write amplification, WAL flushesGroup commit, sequential append logs, and bounded buffer-pool pinning
Intra-Region RPC Roundtrip0.5 ms – 2.5 msSerialization, 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:

  1. 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.
  2. How is backpressure enforced when demand exceeds capacity? Unbounded in-memory queues, unthrottled retries, and blocking calls without explicit deadlines turn a localized 100 ms slowdown into a fleet-wide Out-Of-Memory (OOM) cascade. Always bound queue depths, propagate caller deadlines, and shed excess load early.
  3. 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.

Spotted a mistake or something broken? Report a problem

Not started

Sign in to save your learning progress.

Sign in to save