CMD Guide
HomeConcurrencyConcurrency Foundations

Program, Process, and Thread A Quick Look

A process is isolated from every other process because the OS gives it a private virtual address space — the MMU translates its addresses through a per-process page table, so process A's pointer 0x4000 and process B's pointer 0x4000 resolve to different physical RAM; threads inside one process share that same page table, which is exactly why creating a thread and switching between threads is cheap, while switching between processes is not.

Everything else flows from that one fact. A program is just the bytes of an executable sitting on disk (an ELF or PE file): code, initial data, a symbol table. It does nothing. When the OS exec()s it, the kernel builds a fresh page table, maps the program's segments into virtual memory, allocates a stack and heap, and creates one thread of execution — that live instance is the process. A thread is a single flow of control inside a process: its own program counter, registers, and stack, but sharing the process's heap, global data, open files, and that page table with its sibling threads.

What each one actually owns

The useful question is never "what is it" but "what does it have its own copy of, and what does it share." That table is the whole concept:

ResourceProgram (on disk)ProcessThread (within a process)
Virtual address space / page tablePrivate, ownShared with siblings
Heap & global/static dataInitial image onlyOwnShared with siblings
Code (text segment)The file itselfOwn mappingShared
StackOne (main thread's)Own, private
Registers & program counterPer-threadOwn, private
Open file descriptors / socketsOwn tableShared
OS scheduling unit(holds threads)This is what runs

Note the last row: on modern OSes the thread is the unit the scheduler dispatches onto a CPU. A single-threaded process is just a process with exactly one thread. Sharing the heap is the source of both the speed (no copying, no IPC) and the danger (data races) of threads.

The mechanism: why a thread switch is cheap and a process switch is not

Address translation goes virtual → physical on every memory access. To stay fast, the CPU caches recent translations in the TLB (Translation Lookaside Buffer). The TLB is keyed by the current address space. When the kernel switches the CPU to a thread in a different process, it must load that process's page-table root register (CR3 on x86), which invalidates TLB entries that belonged to the old space. The next thousands of memory accesses then miss the TLB and pay for page-table walks until it warms up again — that cold-cache penalty, not the register save itself, is the real cost of a process switch.

Switching between two threads of the same process never touches CR3: same page table, same TLB, same caches stay warm. You only swap the cheap per-thread state.

diagram
diagram

A traced context switch with real numbers

Suppose threads A1 and A2 (both in Process A) and B1 (in Process B) are runnable on one core. The scheduler gives each a time slice, then switches. Here is what the kernel does and roughly what it costs (order-of-magnitude figures on a modern x86 server; your hardware varies):

StepA1 → A2 (same process)A2 → B1 (cross process)
1. Save A's registers + PC to its kernel stack~done~done
2. Pick next thread (scheduler)~done~done
3. Reload page-table root (CR3)SKIPPED (same address space)Required
4. TLB state after switchStays validFlushed (or tagged-miss)
5. Restore next thread's registers + PC~done~done
Direct switch cost~1–2 µs~2–4 µs
Indirect cost (TLB + cache refill over next ~100µs)~near zero~tens of µs of stalls

The direct numbers look close. The story is in the last row: after A2 → B1, the next few thousand memory accesses by B1 miss a cold TLB and cold L1/L2, so the effective cost of the cross-process switch can be 10–30× the same-process switch. Multiply by hundreds of switches per second and this is why a server pinned with many processes thrashing on one core spends real CPU just shuffling address spaces. Thread-per-request servers and goroutine schedulers exist largely to dodge this.

The concurrent search, in code (Java and Go)

The lesson's running example — split a scan of 1,000,000,000 records across workers — makes the shared-vs-private split concrete. Each worker reads a disjoint slice (shared heap, no copy) and writes a private partial count, then we combine. Same algorithm, two runtimes.

Java — threads = OS threads, shared heap, join to combine

// Java 17. Each worker scans a disjoint range and returns its count.
import java.util.concurrent.*;
import java.util.*;

public class ParallelScan {
    static final long TOTAL = 1_000_000_000L;
    static final int WORKERS = 4;

    // matchesIn must be PURE per range: reads shared data, writes only locals.
    static long matchesIn(long lo, long hi) {
        long count = 0;
        for (long i = lo; i < hi; i++) {
            if ((i & 0xFFFF) == 0) count++;   // stand-in predicate
        }
        return count;
    }

    public static void main(String[] args) throws Exception {
        var pool = Executors.newFixedThreadPool(WORKERS);
        long slice = TOTAL / WORKERS;
        List<Future<Long>> futures = new ArrayList<>();
        for (int w = 0; w < WORKERS; w++) {
            final long lo = w * slice;
            final long hi = (w == WORKERS - 1) ? TOTAL : lo + slice;
            futures.add(pool.submit(() -> matchesIn(lo, hi)));
        }
        long total = 0;
        for (Future<Long> f : futures) total += f.get(); // combine
        pool.shutdown();
        System.out.println("matches = " + total);
    }
}

Go — goroutines = green threads, channels to combine

// Go 1.22. Each goroutine scans a disjoint range; results flow over a channel.
package main

import "fmt"

const (
    total   = 1_000_000_000
    workers = 4
)

func matchesIn(lo, hi int) int {
    count := 0
    for i := lo; i < hi; i++ {
        if i&0xFFFF == 0 { // stand-in predicate
            count++
        }
    }
    return count
}

func main() {
    results := make(chan int, workers)
    slice := total / workers
    for w := 0; w < workers; w++ {
        lo := w * slice
        hi := lo + slice
        if w == workers-1 {
            hi = total
        }
        go func(lo, hi int) { results <- matchesIn(lo, hi) }(lo, hi)
    }
    sum := 0
    for i := 0; i < workers; i++ {
        sum += <-results // combine
    }
    fmt.Println("matches =", sum)
}

Where the runtimes differ. Each Java Thread is backed by a real OS thread with its own ~1 MB stack; spawning thousands is expensive and the OS scheduler does the cross-thread switching described above. A Go goroutine starts with a tiny ~2–8 KB growable stack and is multiplexed by the Go runtime onto a small pool of OS threads (the M:N scheduler), so launching 100,000 goroutines is routine. Java combines results by sharing a Future handle (the worker writes, get() reads, with a happens-before edge); Go's idiom is to not share — "share memory by communicating" — passing each partial over a channel, which is itself the synchronization. Java's classic primitive for hand-off is wait/notify on a monitor; Go's is the channel send/receive.

Why the naive shared-counter version is wrong

The natural instinct is to give every worker a pointer to one shared total and have each do total++. That breaks, because the workers share the heap. total++ is not atomic — it compiles to read, add, write. Two threads can both read 41, both compute 42, both write 42, and one increment is silently lost. The fix in the code above is structural: each worker accumulates into a private local (its own stack, unshared) and we combine only at the end via a Future (Java) or a channel (Go), so there is exactly one writer per slot. "Partition into private state, merge once" sidesteps the race entirely instead of paying for a lock on every increment.

Pitfalls

Takeaways


Sources: Silberschatz, Galvin & Gagne, Operating System Concepts, 10th ed. (processes, threads, context switching, the TLB); Bryant & O'Hallaron, Computer Systems: A Programmer's Perspective, 3rd ed. (virtual memory, page tables, address translation); the Linux clone(2) / fork(2) manual pages and Intel SDM Vol. 3 (CR3 and TLB behavior); Brian Goetz et al., Java Concurrency in Practice (thread cost, the memory model, lost updates); the Go documentation and "Effective Go" (goroutines, the M:N scheduler, "share memory by communicating"). Re-authored and deepened for this guide — added the address-space isolation mechanism, the cost-of-context-switch trace, a hand-authored diagram replacing the GIF, the lost-update fix, and the Java/Go side-by-side.

🤖 Don't fully get this? Learn it with Claude

Stuck on Program, Process, and Thread A Quick Look? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **Program, Process, and Thread A Quick Look** (Concurrency) and want to truly understand it. Explain Program, Process, and Thread A Quick Look from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **Program, Process, and Thread A Quick Look** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **Program, Process, and Thread A Quick Look** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **Program, Process, and Thread A Quick Look** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes