Chapter F.7: Concurrency, the Mental Model§

Authors: Shrey Patel and Jay Patel, Coconut Labs
Book: Think Like an OS. See book/00-INDEX.md

This chapter builds the userspace mental model of concurrency: what threads actually share, why counter++ is a lie, what "atomic" does and does not promise, how a mutex works as a discipline rather than a magic word, and why two perfectly reasonable threads can freeze each other forever. By the end you will have written a data race, watched it destroy data, caught it with a tool, fixed it two different ways with measured costs, and built a real deadlock on purpose. Two kinds of reader need this chapter. If you are heading for kernel work, Chapter 10 is unreadable without it. If you are heading for low-latency trading, the lab is where you measure on your own machine what a contended lock costs against one atomic instruction, and why four cores sharing one counter are still fighting over a single cache line. Chapter 10 takes this exact model into the kernel, where the stakes are higher and the tools are stranger. The rules are the same rules you learn here.

The problem§

Every chapter after this one describes a program that is being run by many actors at once. The kernel is the most concurrent program on your machine: at any instant it may be executing on every CPU core simultaneously, on behalf of different processes, while interrupt handlers barge in between any two instructions. You cannot read kernel code (scheduler, filesystem, driver, anything) without constantly asking "who else might be touching this data right now?" That question, and the machinery for answering it safely, is what this chapter installs.

If you come from web, data, or mobile work, you have already lived with concurrency: promises, async/await, goroutines, thread pools your framework manages for you. What those environments mostly hide is the layer underneath: raw threads sharing raw memory, where nothing referees access and the hardware itself will happily interleave two half-finished updates. That layer is where the kernel lives full-time. Chapter 10 (kernel concurrency: spinlocks, RCU, memory barriers, interrupt context) assumes you already flinch at an unprotected shared variable. This chapter is where the flinch gets trained.

Unfolded§

Concurrency is structure; parallelism is hardware§

Two words that get used interchangeably and should not be.

Concurrency means your program is structured as multiple activities that are logically in flight at the same time. They may or may not literally run at the same time. A single CPU core can run a concurrent program by rapidly switching between activities: run thread A for a millisecond, pause it, run thread B, switch back. Each thread makes progress; nothing ever executes simultaneously. This is called interleaving.

Parallelism means activities literally executing at the same instant, which requires multiple execution units, meaning multiple CPU cores. Parallelism is a property of the hardware execution; concurrency is a property of the program's structure.

ConcurrencyParallelism
What it isMultiple logical activities in flightMultiple activities physically executing at once
Needs multiple cores?No; interleaving on one core is enoughYes
Who provides itProgram structure (threads, tasks, async)Hardware (cores) + a scheduler that spreads work
ExampleNode.js event loop juggling 1,000 requests on one coreMatrix multiply split across 16 cores

The part that bites: almost every bug in this chapter can happen without parallelism. A single-core machine that switches between two threads at an unlucky instruction boundary produces the same lost updates and deadlocks as a 128-core server. Parallelism only makes the unlucky timings vastly more frequent. So the mental model to adopt is: between any two instructions of my thread, anything can happen in another thread. Not "might on a bad day". Assume it does.

Processes vs threads: what is actually shared§

A process (Chapter F.4 territory) is a running program with its own private virtual address space: its own view of memory, enforced by the hardware. Two processes cannot see each other's variables. If process P1 has a global counter at address 0x5000, and P2 has one at the same address, those are two different physical locations, because the page tables translate them differently. Isolation is the default; sharing between processes requires deliberate acts (pipes, sockets, explicitly shared memory segments).

A thread is an execution context inside a process: its own program counter, its own registers, its own stack, and nothing else of its own. All threads in a process share one address space: the same code, the same globals, the same heap. When thread T1 writes to a global, T2 sees the write, because "the global" is one memory location for both of them.

   Process P1              Process P2            One process, two threads
┌────────────────┐     ┌────────────────┐     ┌────────────────────────────┐
│ code           │     │ code           │     │ code (shared)              │
│ globals        │     │ globals        │     │ globals (shared)           │
│ heap           │     │ heap           │     │ heap (shared)              │
│ stack          │     │ stack          │     │ ┌──────────┐ ┌──────────┐  │
└────────────────┘     └────────────────┘     │ │ T1 stack │ │ T2 stack │  │
 own page tables        own page tables       │ │ T1 regs  │ │ T2 regs  │  │
 (isolated)             (isolated)            │ └──────────┘ └──────────┘  │
                                              │  one set of page tables    │
                                              └────────────────────────────┘

From the programmer's view this is a trade:

ProcessesThreads
MemoryIsolated by defaultShared by default
CommunicationExplicit (pipes, sockets, shm)Implicit; touch the same variable
A bug in oneUsually containedCan corrupt every thread's data
Creation/switch costHeavierLighter
Failure mode you fearSerialization/IPC bugsData races

Threads share everything, which is exactly why they are fast to communicate through and exactly why the rest of this chapter exists. Nothing checks your accesses. The hardware will not warn you. The compiler assumes you followed rules nobody forced you to follow, and it is entitled to assume that. The data-race definition below says why.

The data race: counter++ is three instructions§

Here is the canonical disaster, and it fits in one line:

c
counter++;    /* two threads run this concurrently */

counter++ looks atomic: one token, one operation. But the CPU cannot increment memory in place as one indivisible step (on most paths it does not, and the compiler does not ask it to). The compiler lowers it to three machine instructions:

LOAD   r, counter     ; read current value from memory into a register
ADD    r, 1           ; bump the private register copy
STORE  counter, r     ; write the register back to memory

A register is a small private scratch slot inside the CPU core, and each thread's registers are its own (they are saved and restored when the OS switches threads). So the middle of an increment happens in private, and only the LOAD and STORE touch the shared world. Now interleave two threads:

counter starts at 0. Both threads execute counter++.

Thread A                       Thread B                      counter (memory)
────────────────────────────   ───────────────────────────   ────────────────
LOAD  rA, counter   (rA = 0)                                        0
                               LOAD  rB, counter  (rB = 0)          0
ADD   rA, 1         (rA = 1)                                        0
                               ADD   rB, 1        (rB = 1)          0
STORE counter, rA                                                   1
                               STORE counter, rB                    1  ◀── A's
                                                                        increment
Two increments executed. Final value: 1.                                lost

B read the value before A wrote its result back, so B's STORE overwrites A's. One increment evaporates. This is the lost update. Run four threads doing a million increments each and you will not get 4,000,000. You will get some smaller number, different on every run, because the loss count depends on exactly how the OS happened to slice the threads that time. You will build exactly this in the Lab.

Now the precise vocabulary, because the two standard terms are related but not the same:

Keep the distinction: data race = unsynchronized memory access, defined by the language standard, detectable by tools; race condition = timing-dependent wrongness, defined by your intent, detectable only by understanding the program.

Atomicity: the promise, the hardware, the portable interface§

An operation is atomic if no other thread can ever observe it half-done: it either has not happened yet or has fully happened, with no visible intermediate state. If counter++ were atomic (one indivisible read-modify-write) the interleaving above would be impossible, and the second increment would be forced to see the first one's result.

Hardware provides exactly this, as special instructions:

You do not write these instructions by hand. C11 (and C++11) standardized a portable interface: <stdatomic.h>, the _Atomic type qualifier, and functions like atomic_fetch_add(&counter, 1), which the compiler lowers to the right instruction for your CPU (lock xadd on x86-64, ldadd on ARM64 with LSE). By default these operations use the strongest ordering mode, memory_order_seq_cst. Hold that thought for the memory-ordering teaser below.

One warning before you fall in love: atomics make one operation indivisible. They do not make two related operations indivisible. atomic_fetch_add fixes the counter; it does not help when the invariant spans multiple variables ("debit account A and credit account B"). For that you need mutual exclusion.

Critical sections, the mutex, and how deadlock happens§

A critical section is a stretch of code that touches shared data and must not interleave with other threads touching the same data. It must execute as if it were alone. Mutual exclusion is the guarantee that at most one thread is inside at a time. The workhorse primitive is the mutex (MUTual EXclusion lock):

c
pthread_mutex_lock(&m);      /* wait here until I hold the lock */
/* critical section: shared data is mine alone */
pthread_mutex_unlock(&m);    /* release; one waiter may now enter */

lock blocks until the calling thread owns the mutex; between lock and unlock, any other thread calling lock on the same mutex waits. The discipline that makes this work is a convention, not an enforcement: every access to the protected data, reads included, must happen under the lock. One forgetful code path that touches the data bare-handed reintroduces the race, and the compiler will not stop you.

Mutexes create a new failure mode. Suppose two locks A and B, and two threads that each need both:

Thread 1                          Thread 2
────────                          ────────
lock(A)        ✓ got it           lock(B)        ✓ got it
lock(B)        blocks,            lock(A)        blocks,
               B is held by T2                   A is held by T1

        T1 ──waits for──▶ B ──held by──▶ T2
        ▲                                 │
        └────held by── A ◀──waits for────┘

Neither thread can proceed, and neither will ever release what it holds, because release code is after the acquire it is blocked on. This is deadlock, and this two-lock shape is called an ABBA deadlock (T1 takes A then B; T2 takes B then A). It will not hang every run, only when the timing lands both threads inside the window. That makes it a monster to reproduce in testing and reliable only in production.

Deadlock requires four conditions to hold simultaneously. These are the Coffman conditions, after E. G. Coffman Jr., who with M. J. Elphick and A. Shoshani laid them out in the 1971 survey paper "System Deadlocks" (ACM Computing Surveys):

  1. Mutual exclusion. The resources are exclusively held; only one thread can own a lock at a time.
  2. Hold and wait. A thread holds one resource while waiting for another (T1 holds A while waiting for B).
  3. No preemption. Nothing can forcibly take a lock away from its holder.
  4. Circular wait. A cycle of threads each waiting for a resource the next one holds (the loop in the diagram).

All four are necessary; break any one and deadlock cannot occur. In practice you almost always break the fourth: impose a global lock ordering ("A before B, always, in every code path") and a cycle becomes impossible, because a cycle requires someone to acquire against the order. This is not a toy rule: the Linux kernel enforces documented lock orderings across thousands of locks, and has a runtime checker (lockdep) that screams when code acquires out of order. You will build the ABBA hang and fix it by ordering in the Lab.

Beyond the mutex: the rest of the menu§

Reader-writer locks. A mutex is pessimistic: even two pure readers exclude each other, though concurrent reads of unchanging data are harmless. A reader-writer lock (pthread_rwlock_t) splits acquisition into two modes. Read mode admits any number of concurrent readers. Write mode admits one writer and excludes everyone else. It pays off when reads vastly outnumber writes and the critical section is long enough to matter; for short sections the extra bookkeeping can cost more than it saves, and writer starvation (readers arriving forever, writer never admitted) is a classic pitfall.

Condition variables. A mutex answers "may I touch this data?"; a condition variable (pthread_cond_t) answers "may I sleep until this data is in the state I need?", which is what a consumer waiting for a queue to be non-empty needs. The waiting side must always look like this:

c
pthread_mutex_lock(&m);
while (!predicate)                  /* while, never if */
    pthread_cond_wait(&cv, &m);    /* atomically: unlock m, sleep; relock m on wakeup */
/* predicate is true, and we hold the lock */
pthread_mutex_unlock(&m);

The while instead of if is mandatory for two reasons. First, POSIX explicitly permits spurious wakeups: pthread_cond_wait may return even though nobody signaled, and the standard's own rationale says allowing this forces applications into the predicate-testing loop they need anyway. Second, even after a genuine signal, another thread may sneak in and consume the condition between your wakeup and your reacquisition of the mutex. So: wake up, recheck, and go back to sleep if the world disappoints you.

Semaphores. A semaphore (Dijkstra's primitive) is a counter with two atomic operations: wait (decrement; block if the count is zero) and post (increment; wake a waiter). A semaphore initialized to 1 behaves like a mutex without an owner; initialized to N it is an admission gate, meaning "at most N threads in this region" (a connection pool, a bounded queue's slot count). The lack of ownership is the sharp edge: any thread can post, so nothing ties release to the acquirer, and the discipline is entirely on you.

Spinning vs sleeping. When a lock is busy, a waiter has two choices. It can sleep, telling the OS to deschedule it until the lock frees, which costs a couple of context switches but burns zero CPU while waiting. Or it can spin, sitting in a tight loop retrying, burning CPU but winning if the wait is shorter than a context switch. That second trade is why busy-wait loops survive in code that looks wasteful from the outside, including hot paths that would rather burn a core than pay for a context switch. Userspace mutexes sleep (with, commonly, a brief opportunistic spin first). Inside the kernel there are contexts where sleeping is forbidden, such as interrupt handlers and code holding certain locks, and there the spinlock is not an optimization but the only legal tool. That story, and why kernel spinlocks must also disable preemption, is Chapter 10's.

Memory ordering: the honest teaser§

Everything above quietly assumed that memory operations happen in the order the source code states, and that a write by one thread becomes visible to others "at that point." Neither is guaranteed. Compilers reorder and eliminate memory accesses aggressively, which they are entitled to do because they may assume no data races. CPUs execute and make stores visible out of program order too. A mechanism called a store buffer is one culprit; Chapter 10 dissects it, and we will not fake it in two sentences here.

What tames this is a formal relation called happens-before: the language standard defines which operations are ordered relative to which, and synchronization primitives are precisely the things that create happens-before edges between threads. A mutex unlock happens-before the next lock of that mutex, which is why data written under a lock is safely visible to the next lock holder. Default C11 atomics give you sequential consistency, the model Leslie Lamport defined in 1979: all threads observe one single interleaving of all operations, consistent with each thread's program order. It is the model your intuition already uses, and it is the most expensive one to provide, which is why the weaker orderings (memory_order_acquire, release, relaxed) exist. Until Chapter 10, the working rule is: use mutexes and default atomics, and never invent synchronization out of plain variables. "I set a flag in thread A and loop reading it in thread B" is a data race and an ordering bug at once.

Everyone defers this one. The kernel keeps an entire in-tree document about it (Documentation/memory-barriers.txt, named again below), which is a fair signal about how much of it fits in a paragraph. You can stop at the working rule and lose nothing before Chapter 10.

Practical hygiene§

Keep critical sections small. Lock, touch the shared data, unlock. Never hold a lock across I/O, allocation you can hoist, or a call into code you do not control. Every instruction under the lock is an instruction during which every contender is stalled.

Lock data, not code. A mutex protects a set of data (these three fields, this list), not a function. Document the association ("guarded by m" next to the field) and you can audit correctness field by field. Locks sprinkled over functions with no stated data mapping are how "we added a lock and it still races" happens.

Prefer message passing when it fits. Many designs need no shared mutable state at all: give each piece of data one owning thread, and let other threads send it requests over a queue (one well-tested concurrent queue instead of a dozen ad-hoc locks). "Do not communicate by sharing memory; instead, share memory by communicating" is the Go proverb, and the idea behind it is older than Go. It goes back to Hoare's Communicating Sequential Processes (1978). This is also the reason Rust matters here: its ownership system makes "who may mutate this, and from where" a compile-time property, so a value has one owner, mutable access is exclusive by construction, and cross-thread sharing must go through types that are explicitly safe to share. Whole classes of the bugs in this chapter become compile errors instead of 3 a.m. pages. Chapter 34 makes that argument properly.

The real thing in Linux§

On Linux, threads are not a separate kernel concept. They are processes that share. pthread_create in glibc's NPTL (Native POSIX Threads Library) calls the clone(2) syscall with a flag bundle: CLONE_VM (share the address space; this single flag is the "threads share everything" of this chapter, made literal), CLONE_FILES (share the file-descriptor table), CLONE_SIGHAND, CLONE_THREAD, and friends. Fork-a-process and spawn-a-thread are the same kernel entry point with different sharing flags. See man 2 clone on man7.org.

A pthread_mutex_t is a futex-based lock ("fast userspace mutex"). The uncontended case, which is the common case, is a single atomic instruction on an integer in userspace, with no kernel involvement at all. Only when a thread must actually wait does it call the futex(2) syscall to sleep, and only an unlock that has waiters calls in to wake one. man 7 futex describes the split. The kernel side lives at kernel/futex/ in the 6.12 tree (kernel/futex/core.c and siblings).

The kernel's own locks for its own data are in kernel/locking/: kernel/locking/mutex.c is the sleeping kernel mutex, kernel/locking/qspinlock.c is the queued spinlock behind spin_lock() (declared via include/linux/spinlock.h). The memory-ordering story you were teased with has a famous in-tree document, Documentation/memory-barriers.txt. And the kernel's answer to ThreadSanitizer is KCSAN, the Kernel Concurrency Sanitizer (Documentation/dev-tools/kcsan.rst), the same idea as the tool you are about to use, rebuilt for a program that cannot link against a runtime library.

Coconut tie-in§

Coconut's new subsystems are born concurrent: the agent registry in kernel/agent/ is hit by agent_spawn/agent_attest (syscalls 472/473) from any CPU at once, and the audit pipeline's BLAKE3 hash chain (04-HLD) is only a chain if appends are strictly serialized. A lost update there is not a wrong counter but a broken tamper-evidence guarantee. The lock-ordering discipline and race-vocabulary from this chapter are the exact review language used on every cap-touching diff, and Chapter 10 upgrades them to the kernel-context rules (spinlocks, RCU, lockdep) that Coconut's code must actually follow.

Lab§

Runs on a macOS host directly (Apple's clang handles everything below; on macOS gcc is an alias for clang) or in a Docker Linux container. The container is the more reproducible path, and it is required if your Apple toolchain refuses -fsanitize=thread:

sh
mkdir -p ~/f7-lab && cd ~/f7-lab
# Optional Linux container (recommended):
docker run --rm -it -v "$PWD":/work -w /work gcc:14 bash

All commands below work in either environment. Five numbered steps, and a written prediction before each one. The last one deliberately hangs; knowing that in advance is part of the exercise.

1. Build the race, watch updates get lost§

Create race.c:

c
#include <pthread.h>
#include <stdio.h>

#define NTHREADS 4
#define NITERS   1000000L

long counter = 0;

void *worker(void *arg) {
    (void)arg;
    for (long i = 0; i < NITERS; i++)
        counter++;                      /* LOAD, ADD, STORE - three steps */
    return NULL;
}

int main(void) {
    pthread_t t[NTHREADS];
    for (int i = 0; i < NTHREADS; i++)
        pthread_create(&t[i], NULL, worker, NULL);
    for (int i = 0; i < NTHREADS; i++)
        pthread_join(t[i], NULL);
    printf("expected %ld, got %ld (lost %ld)\n",
           NTHREADS * NITERS, counter, NTHREADS * NITERS - counter);
    return 0;
}

Predict first: what will got be? Same every run? Now build and run it five times. We compile at -O0 deliberately, to keep the three-instruction shape of counter++ intact (at -O2 the compiler exploits the undefined behavior and collapses the whole loop, which is a lesson in itself; try it afterward).

sh
gcc -O0 -pthread -o race race.c
for i in 1 2 3 4 5; do ./race; done

Expected (your numbers will differ; that is the point):

expected 4000000, got 1371542 (lost 2628458)
expected 4000000, got 1523990 (lost 2476010)
expected 4000000, got 1249877 (lost 2750123)
...

Millions of increments lost, a different count every run. No crash, no warning. Silent data corruption.

2. Make the race visible to tooling: ThreadSanitizer§

sh
gcc -O1 -g -fsanitize=thread -pthread -o race_tsan race.c
./race_tsan

Expected: a report like

WARNING: ThreadSanitizer: data race (pid=...)
  Write of size 8 at 0x... by thread T2:
    #0 worker race.c:12
  Previous write of size 8 at 0x... by thread T1:
    #0 worker race.c:12
...
SUMMARY: ThreadSanitizer: data race race.c:12 in worker

TSan pinpoints the racing line and both stacks. Two notes. The instrumented run is much slower (typical slowdown is 5x to 15x), and the final count may look closer to correct because instrumentation changes the timing. Ignore the count and trust the report. If Apple's clang complains about the runtime, use the Docker path.

3. Fix it with a mutex, and pay for it§

Create mutex.c, the same file with three changes:

c
#include <pthread.h>
#include <stdio.h>

#define NTHREADS 4
#define NITERS   1000000L

long counter = 0;
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;

void *worker(void *arg) {
    (void)arg;
    for (long i = 0; i < NITERS; i++) {
        pthread_mutex_lock(&m);
        counter++;
        pthread_mutex_unlock(&m);
    }
    return NULL;
}

int main(void) {
    pthread_t t[NTHREADS];
    for (int i = 0; i < NTHREADS; i++)
        pthread_create(&t[i], NULL, worker, NULL);
    for (int i = 0; i < NTHREADS; i++)
        pthread_join(t[i], NULL);
    printf("expected %ld, got %ld\n", NTHREADS * NITERS, counter);
    return 0;
}
sh
gcc -O0 -pthread -o mutexed mutex.c
time ./race
time ./mutexed

Expected: mutexed prints exactly got 4000000, every run. Its wall-clock time is much worse than race (commonly an order of magnitude or more on this microbenchmark; your machine will give its own ratio). Four threads hammering one mutex a million times each is close to a worst case: the critical section is one instruction, so nearly all the time is lock traffic. Correctness first; then note the price.

4. Fix it with a C11 atomic, and compare§

Create atomic.c:

c
#include <pthread.h>
#include <stdatomic.h>
#include <stdio.h>

#define NTHREADS 4
#define NITERS   1000000L

_Atomic long counter = 0;

void *worker(void *arg) {
    (void)arg;
    for (long i = 0; i < NITERS; i++)
        atomic_fetch_add(&counter, 1);   /* one indivisible RMW instruction */
    return NULL;
}

int main(void) {
    pthread_t t[NTHREADS];
    for (int i = 0; i < NTHREADS; i++)
        pthread_create(&t[i], NULL, worker, NULL);
    for (int i = 0; i < NTHREADS; i++)
        pthread_join(t[i], NULL);
    printf("expected %ld, got %ld\n", NTHREADS * NITERS, counter);
    return 0;
}
sh
gcc -O0 -pthread -o atomic atomic.c
time ./atomic

Expected: exactly got 4000000, and typically noticeably faster than the mutex version, because the whole lock/unlock protocol is replaced by one atomic instruction (lock xadd on x86-64; ldadd on ARM64 with LSE). Inspect it with objdump -d atomic | grep -A2 worker on Linux, or otool -tv atomic on macOS. Still far slower than a single-threaded loop would be: the cores are fighting over one cache line. Atomics are the right tool when one operation is the whole invariant; the mutex remains the tool when the invariant spans more than one step.

5. Build the ABBA deadlock, then break the cycle§

Create deadlock.c:

c
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

pthread_mutex_t A = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t B = PTHREAD_MUTEX_INITIALIZER;

void *one(void *arg) {
    (void)arg;
    pthread_mutex_lock(&A);
    printf("thread 1: holds A, wants B\n");
    usleep(100000);              /* widen the window so the hang is reliable */
    pthread_mutex_lock(&B);
    printf("thread 1: got both\n");
    pthread_mutex_unlock(&B);
    pthread_mutex_unlock(&A);
    return NULL;
}

void *two(void *arg) {
    (void)arg;
    pthread_mutex_lock(&B);      /* opposite order: B then A */
    printf("thread 2: holds B, wants A\n");
    usleep(100000);
    pthread_mutex_lock(&A);
    printf("thread 2: got both\n");
    pthread_mutex_unlock(&A);
    pthread_mutex_unlock(&B);
    return NULL;
}

int main(void) {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, one, NULL);
    pthread_create(&t2, NULL, two, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    printf("done\n");
    return 0;
}

Predict: which lines print? Then:

sh
gcc -O0 -pthread -o deadlock deadlock.c
./deadlock

Expected:

thread 1: holds A, wants B
thread 2: holds B, wants A
(hangs forever - Ctrl-C to kill it)

All four Coffman conditions are now standing in front of you: exclusive locks, each thread holding one while waiting, no preemption, and a two-node cycle. Now break circular wait: edit two() so it also locks A first, then B (same global order as thread 1), rebuild, rerun. Expected: all four messages plus done, every run. As a bonus, rebuild the broken version with -fsanitize=thread. TSan reports lock-order inversions ("potential deadlock") even on runs that happen not to hang, which is exactly what makes it useful: the bug is visible without the unlucky timing.

Count what you are holding now. You can write the three-instruction shape of counter++ from memory and point at the interleaving that loses one. You can run TSan against code you did not write and read what it says. You have your own numbers for what a contended mutex costs against one atomic instruction on your own hardware, which beats any rule of thumb somebody hands you. And you have made a deadlock happen on purpose, which is the only reliable cure for being surprised by one. Chapter 10 keeps all four and changes the context: no sleeping in interrupt handlers, spinlocks where mutexes are illegal, RCU for read-mostly data, and lockdep watching your acquisition order the whole time.

Bridge notes§

If you have OS + architecture coursework, the definitions here are old friends: skim the concurrency/parallelism split, the three-instruction interleaving, the Coffman conditions (you likely proved things about them), and semaphores. What is likely genuinely new or sharper than the whiteboard version: (1) the C11 formalization, where a data race is undefined behavior rather than merely a stale read, which licenses the compiler transformations that make racy code weirder than any interleaving diagram; (2) the TSan workflow as a daily tool, including its lock-order-inversion reports, since coursework proved deadlock exists and TSan finds yours before it fires; (3) the measured cost hierarchy from the lab (racy < atomic < contended mutex) and the futex design that makes an uncontended pthread_mutex_lock never enter the kernel, which means the syscall-per-lock mental model many courses leave you with is wrong on Linux; (4) the ARMv8.1 LSE naming if your architecture course predates it, with ldadd/cas/swp replacing the ldxr/stxr retry loop. The memory-ordering section is deliberately a teaser; your acquire/release intuition from coursework gets its full workout in Chapter 10.

Sources§