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:
| Resource | Program (on disk) | Process | Thread (within a process) |
|---|---|---|---|
| Virtual address space / page table | — | Private, own | Shared with siblings |
| Heap & global/static data | Initial image only | Own | Shared with siblings |
| Code (text segment) | The file itself | Own mapping | Shared |
| Stack | — | One (main thread's) | Own, private |
| Registers & program counter | — | Per-thread | Own, private |
| Open file descriptors / sockets | — | Own table | Shared |
| 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.
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):
| Step | A1 → 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 switch | Stays valid | Flushed (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
- Treating shared-heap mutation as safe because "it's just one variable." Any unsynchronized write to shared state that another thread reads is a data race; on the JVM it can even surface as a stale value forever, not just a lost update, because of the memory model and caching. Threads share the heap — that is the feature and the trap.
- Spawning an OS thread (or process) per unit of work. A thread costs ~1 MB of stack and a scheduler slot; tens of thousands will exhaust memory or drown the core in context switches. Use a bounded pool (Java) or goroutines (Go). Spawning processes per request is worse still — full address-space setup plus the cross-process switch cost above.
- Assuming threads give you isolation. One thread that corrupts the heap, leaks a file descriptor, or segfaults takes down the whole process and every sibling thread with it. Processes are the fault-isolation boundary (this is why Chrome puts tabs in separate processes); threads are not.
- Forgetting the slice boundary. Integer division
TOTAL / WORKERSdrops the remainder. The code gives the last workerhi = TOTALso no records are skipped — a classic off-by-N that silently undercounts if you writehi = lo + slicefor every worker. - Confusing the program with the process. Running the same executable twice gives you two processes with separate address spaces — they do not share globals. People debugging "my static cache isn't shared" are often running two JVMs, not two threads.
Takeaways
- A program is passive bytes on disk; a process is a live instance with its own private virtual address space (page table); a thread is a flow of execution that shares that address space with its siblings but keeps its own stack, registers, and PC.
- Isolation between processes is a hardware mechanism: per-process page tables mean the same virtual address maps to different physical RAM. That isolation is exactly what makes cross-process switches expensive (CR3 reload + TLB flush + cold caches).
- Thread switches stay cheap because they never change the address space — same page table, warm TLB. This is why concurrency within one process uses threads/goroutines, not processes.
- Sharing the heap is the source of both speed (no copy/IPC) and danger (data races). Prefer "partition into private state, merge once" — Java
Futures, Go channels — over locking shared mutable state on every operation.
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.
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.
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.
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.
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.