Threads and Concurrency

A thread lets one process do several things at once — and the moment you have two threads touching the same memory, you've entered the hardest territory in all of programming: concurrency. Race conditions, deadlocks, and the need for synchronization are not exotic edge cases; they're the fundamental consequences of shared mutable state, and understanding them is what separates working concurrent code from code that fails mysteriously.

A process (last post) has one line of execution by default. Threads let a single process do multiple things concurrently, sharing its memory. That sharing is powerful and dangerous — it’s the source of concurrency’s hardest problems. This post covers threads vs processes, why concurrency is hard (race conditions), synchronization primitives, and deadlock. It’s foundational for understanding concurrent programs, and it connects to the Rust series (fearless concurrency), distributed systems (data races), and everyday backend performance.

Threads vs processes

A thread is a unit of execution within a process. A process starts with one thread but can have many, and the key distinction from processes is what’s shared:

Process
├── shared: code, heap, globals, open files   ← all threads see the same memory
├── Thread 1: own stack + registers
├── Thread 2: own stack + registers
└── Thread 3: own stack + registers

This sharing is the whole point and the whole problem:

So threads trade processes’ safe isolation for shared-memory efficiency and cooperation — and that trade is where concurrency’s difficulty comes from. (Processes vs threads is a real design choice: processes for isolation/safety, threads for shared-memory efficiency; and there are lighter models still, like async/coroutines, and Rust’s ownership-checked threads.)

Why concurrency is hard: race conditions

The fundamental problem of concurrency is the race condition — when the correctness of the result depends on the timing of how threads interleave, and some interleavings produce wrong results. The classic example: two threads incrementing a shared counter:

counter = 0; two threads each do: counter = counter + 1   (expect final counter = 2)

But "counter = counter + 1" is really THREE steps: read counter, add 1, write counter.
If the threads interleave:
   Thread A reads counter (0)
   Thread B reads counter (0)      ← both read 0 before either writes!
   Thread A writes 1
   Thread B writes 1               ← final counter = 1, not 2. One increment LOST.

The bug: counter = counter + 1 is not atomic — it’s read-modify-write, and if two threads interleave between the read and the write, one update is lost. The result depends on timing (which is nondeterministic), so the bug is intermittent — it might work a million times and fail once, under load, unreproducibly. This is what makes concurrency bugs so hard: they’re timing-dependent, nondeterministic, and rarely reproduce on demand.

The general problem is shared mutable state accessed concurrently. Any time multiple threads read and write the same data without coordination, and at least one writes, you have a potential race. This is exactly the data-race problem from the distributed-systems and Rust series — and it’s why Rust’s ownership rules (one writer XOR many readers) exist: to prevent it at compile time. The critical section is the piece of code that accesses shared data and must not be run by two threads simultaneously; protecting critical sections is what synchronization is for.

Synchronization primitives

To make concurrent access to shared data safe, you use synchronization primitives that coordinate threads. The main ones:

The core idea: synchronization coordinates threads’ access to shared state so races can’t happen — most fundamentally by mutual exclusion (a mutex ensuring one-at-a-time access to a critical section). But synchronization has costs: it serializes (threads waiting on a lock aren’t running in parallel — reducing the concurrency benefit), and it introduces new failure modes (deadlock, below). Concurrency is a balance: enough synchronization for correctness, not so much that you lose the parallelism you wanted.

Deadlock and the cost of synchronization

Synchronization prevents races but creates its own hazard: deadlock — when threads are stuck forever, each waiting for a resource another holds. The classic case: two threads, two locks, acquired in opposite orders:

Thread A: locks X, then wants Y
Thread B: locks Y, then wants X
   → A holds X waiting for Y; B holds Y waiting for X → both wait forever. DEADLOCK.

Deadlock is a real, common concurrency bug, and avoiding it requires discipline (e.g. always acquire locks in a consistent order, minimize lock scope, avoid holding multiple locks). More broadly, synchronization has costs that shape concurrent design:

This is why modern languages and systems offer safer concurrency models: Rust’s ownership-based fearless concurrency (compile-time race prevention — the Rust series), message-passing (share by communicating, not by sharing memory — Go’s model, and the actor model), and lock-free/immutable-data approaches. All are responses to “shared mutable state with locks is hard and error-prone.” Understanding the fundamentals here — races, mutexes, deadlock, contention — is what lets you use those higher-level models well and diagnose concurrency problems when they arise.

Threads and concurrency, understood

The takeaway: threads let one process do multiple things concurrently by sharing its memory — powerful (lightweight, cooperative, parallel) but dangerous, because shared mutable state accessed concurrently causes race conditions (timing-dependent, intermittent, nondeterministic bugs — the hardest kind). Synchronization primitives (mutexes for mutual exclusion, atomics, semaphores, condition variables) coordinate access to prevent races, but at the cost of serialization, contention, and new hazards like deadlock. This fundamental difficulty is why safer models exist (Rust’s fearless concurrency, message passing, lock-free structures) — and understanding the fundamentals is what makes those models and your concurrent code comprehensible. Concurrency is genuinely hard, and this is why. The next post covers how the OS decides which thread/process runs when: scheduling.

Key takeaways

Further reading

Sources & References