Threads & Concurrency Explained

September 12, 2026 • 5 min read

Threads & Concurrency Explained
Table of Contents

Part of the series:Operating Systems

The processes post ended with a complaint: a process is a heavy tenant, with its own address space and page table, expensive to create and switch. Programs that need many parallel activities reach for something lighter: the thread1. This post opens that lighter thing up. We will look at what a thread shares and what it keeps private, why shared memory breeds race conditions, the locks that restore order, how locks can freeze each other in deadlock, and the discipline that keeps concurrent programs alive.

Threads vs processes: tenants vs roommates

A process isolates; a thread shares. Threads in the same process run in the same address space: same code, same heap, same open files. What each thread keeps private is only what it needs to pause and resume independently: a program counter, a stack, and registers. Everything else is communal.

process (isolated)              threads (sharing)
──────────────────              ──────────────────
own address space               shared address space,
own page table                  shared heap and files
own files                       ─────────────
                                per thread: stack,
                                registers, program counter

The payoff is speed and easy communication. Creating a thread skips the page table and address-space setup; switching threads can skip the memory-unit reconfiguration; and threads exchange data by writing to shared memory instead of piping bytes through the kernel. The price is exactly that sharing: no isolation means every thread can corrupt every other thread’s data, accidentally, at full speed. Processes protect you from each other by default; threads offer no protection at all.

Races: when sharing goes wrong

A race condition2 happens when the result depends on the timing of uncontrollable events: two threads touching the same data with at least one writing, and no ordering enforced between them. Consider the simplest shared counter:

// shared: int balance = 100;
void deposit(int amount) {
    int t = balance;      // read
    t = t + amount;       // modify
    balance = t;          // write
}

Run one deposit alone and 100 becomes 150. Run two deposits of 50 concurrently and the answer can be 150 instead of 200: both threads read 100, both add 50, both write 150. The three steps each run correctly; the interleaving is wrong. This read-modify-write pattern is the classic race, and it hides everywhere counters, lists, and caches are shared.

Worse, races are intermittent. The same program passes a thousand tests, then fails in production under load, because the failing interleaving needs a timer interrupt at exactly the wrong instruction. Programs that “usually work” are the signature of a race, and testing alone cannot catch them: the space of interleavings is far larger than any test suite.

Mutual exclusion: taking turns

The fix for a race is mutual exclusion3: guarantee that only one thread at a time executes the critical section touching shared data. The standard tool is the mutex4 (mutual-exclusion lock): lock before entering, unlock after leaving. The OS (or threading library) parks waiters instead of spinning them, so a blocked thread costs no CPU while it waits.

pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
int balance = 100;

void deposit(int amount) {
    pthread_mutex_lock(&m);    // wait your turn
    balance = balance + amount;
    pthread_mutex_unlock(&m);  // next, please
}

A semaphore5 generalizes the idea to a counted resource: it holds N permits, waiters take one, leavers return one. A mutex is the N = 1 case (binary semaphore, plus ownership rules). Semaphores shine for producer-consumer queues and connection pools: N slots, N permits, no counting by hand.

Two rules keep locks honest. Hold them briefly (a lock serializes everything waiting on it, so long sections destroy the parallelism threads were bought for), and always pair lock with unlock on every path, including error returns, or the next thread waits forever.

Deadlock: everyone waits for everyone

Locks compose badly. A deadlock6 is a circle of waiting: thread A holds lock 1 and wants lock 2, thread B holds lock 2 and wants lock 1, and neither ever yields. Both sleep forever, holding what the other needs.

thread A                  thread B
────────                  ────────
lock(1) ✓                 lock(2) ✓
          ... time passes ...
want lock(2) ✗            want lock(1) ✗
  (held by B)               (held by A)
     ↓                         ↓
  waits forever             waits forever

Four conditions must all hold for deadlock (the Coffman conditions): mutual exclusion (resources need exclusive access), hold-and-wait (threads hold resources while requesting more), no preemption (nobody can forcibly take a held lock away), and circular wait (the chain of “waits for” loops back). Break any one and deadlock becomes impossible. The practical fix targets the last: impose a global lock order (always take lock 1 before lock 2, everywhere) so circles cannot form. Alternatives include lock timeouts with backoff, or designs that need only one lock at a time.

Deadlock’s cousins deserve a mention. Livelock is motion without progress (two threads keep yielding to each other, forever polite, never done). Starvation is indefinite postponement (a low-priority thread never wins the lock against greedy rivals). Fairness, priorities, and bounded waiting are the scheduling post’s territory; the mechanism is noted here so the symptom is recognized.

The big picture

Threads trade isolation for speed: shared address space, private stacks, cheap creation, instant communication, and zero protection. Sharing without discipline produces races, intermittent and untestable; mutexes and semaphores restore order by serializing critical sections; and locks in combination demand a global order, or circles of waiting freeze the program solid.

But who decides which thread runs next, on which core, for how long, and at what priority? Threads made concurrency possible; the scheduler decides what it costs. That is the next post’s subject.

Footnotes

  1. Thread (computing) - Wikipedia

  2. Race condition - Wikipedia

  3. Mutual exclusion - Wikipedia

  4. Lock (computer science) - Wikipedia

  5. Semaphore (programming) - Wikipedia

  6. Deadlock - Wikipedia