Why Concurrency Needs Real Synchronization

Building an Operating System Kernel: Concurrency, I/O & Synchronization

Chapter 1 · Why Concurrency Needs Real Synchronization

Course 1 built a kernel where every context switch is a clean, sequential handoff — nothing ever runs "at the same instant" as anything else. That's exactly why nothing in that kernel ever needed real synchronization. The moment two processes genuinely share memory — a real IPC page, mapped into both of their page tables — that clean handoff stops being enough. This chapter reproduces the exact problem, deterministically, before Chapter 2 builds the real fix.

A Real Shared Page Between Two Processes

Two processes each map the same physical frame into their own page table at virtual address 0 — a genuine shared page, built entirely from Course 1's own PageTable.map_page(). An increment is modeled as three real, separate steps against it:

def execute_step(cpu, pcb, op, shared_paddr, mem): if op == 'LOAD': cpu.registers['R1'] = mem.data[shared_paddr] elif op == 'INC': cpu.registers['R1'] += 1 elif op == 'STORE': mem.data[shared_paddr] = cpu.registers['R1']

Finding 1: A Deterministic, Exact Reproduction of a Lost Update

Verified directly — two real increments, one silently vanishes
Process A executes LOAD (reads the shared counter, 0, into its own R1) — then a real context switch happens, right there, mid-increment. Process B runs a full, uninterrupted increment: the counter is now 1. Switching back to A, its own R1 still holds the stale value from before B ever ran. A finishes INC+STORE — and the counter ends at 1, not 2. Two real, completed increments; one of them completely erased. This isn't a rare timing accident — this exact interleaving produces this exact result every single time.

Finding 2: Real, Measured Lost Updates Under Real Preemption

Verified directly — 100 of 200 real increments lost, QUANTUM=1
Two processes each run 100 real increments (300 total LOAD/INC/STORE steps apiece) through Course 1's own preemptive kernel, switching after every single instruction. Expected final value: 200. Measured: 100 — exactly half the real work vanished, purely from interleaving two processes' own unsynchronized read-modify-write sequences against the same shared frame.

Finding 3: A Coincidentally Aligned Quantum Hides the Bug Completely

What if the scheduler's own quantum happens to match the length of one full increment (3 steps)?

Verified directly — the exact same unsynchronized code, three different outcomes
QUANTUM=3: every switch lands between increments, never inside one — the counter comes out perfectly correct, 200 of 200. QUANTUM=7 (doesn't align with the 3-step cycle): real lost updates return, 71 of them. Nothing about the increment code changed between these runs — only the quantum did. Code that only works correctly for certain quantum values isn't correct — it's correct by coincidence, and that's exactly why "pick a safe-looking quantum" can never be a real fix.

Where This Connects

This chapter's findingWhat it connects to
The shared page itselfCourse 1's own PageTable.map_page() — reused completely unchanged, mapping one real frame into two processes at once
The forced interleavingCourse 1 Chapter 8's own preemptive kernel — reused directly to produce a real, measured race rather than a hypothetical one
A quantum-dependent bugCourse 1 Chapter 9's own "a mechanism existing isn't the same as it being wired in" lesson — here, the ABSENCE of a mechanism can look like correctness purely by accident
The fix this motivatesChapter 2's own mutex — a real, general-purpose way to guarantee LOAD/INC/STORE always runs as one indivisible unit, regardless of quantum

Hands-On Exercises

Exercise 1

Add a third process with its own private page (never shared) alongside the two racing processes. Confirm the third process's own data is completely unaffected by the race, and explain precisely why.

📄 View solution
Exercise 2

Reproduce Finding 1's own exact interleaving, but with one process incrementing and the other decrementing the shared value. Confirm a real update is still lost, and explain why the race doesn't care which direction the operation moves the value.

📄 View solution
Exercise 3

Run the same unsynchronized race with 2, 3, and 4 processes (30 increments each, QUANTUM=1) sharing one counter. Measure and report the final value and the number of lost updates for each, and explain the genuinely surprising pattern the results show.

📄 View solution

Chapter 1 Quick Reference

  • A shared page: the same physical frame, mapped into two different processes' page tables — a real IPC mechanism, built entirely from Course 1's own code
  • An increment is 3 real steps: LOAD (read), INC (modify), STORE (write back) — a genuine "critical section" a context switch can land inside
  • Verified Finding 1: a deterministic, exact reproduction of a lost update — one process's completed work silently erased by another's stale register
  • Verified Finding 2: real, measured lost updates under real preemption — 100 of 200 increments lost at QUANTUM=1
  • Verified Finding 3: a coincidentally-aligned quantum can hide the exact same bug completely — proving correctness can never depend on scheduler timing
  • Next chapter: Building a Mutex From Scratch — a real, general-purpose fix that works regardless of the quantum