Threads VS Processes

Introduction
In operating system terms, a thread is the smallest sequence of programmed instructions that a CPU can manage independently — a path of execution within a program. Imagine your program as a highway and threads as its lanes: each lane can carry its own traffic simultaneously. Or picture a restaurant, where the chef (one thread) cooks while the waiter (another thread) serves. Both tasks proceed at once, and the restaurant's throughput comes from that overlap.
The analogy gets you started, but the engineering decisions — thread or process, how many, communicating how — depend on what these things actually are in the operating system: what memory they own, what a switch between them costs, and what failure modes each one exposes. That's what this post covers.
What is a Process?
A process is a program in execution, and concretely it is a bundle of resources the kernel tracks: a private virtual address space (its own view of memory, enforced by the CPU's memory management unit), a table of open file descriptors, environment variables, security credentials, and at least one thread of execution. The address space is the defining feature. Two processes can both believe they own the memory at address 0x7f8a..., and the MMU's page tables translate each one's accesses to different physical memory. Neither can read the other's data — not as a convention, but as a hardware guarantee.
That isolation is why a crash in your browser doesn't take down your text editor, and it's also why process creation costs what it costs. On Unix systems a process is created with fork(), which conceptually duplicates the parent — but modern kernels make this cheap with copy-on-write: parent and child initially share all physical pages, and the kernel only copies a page when one of them writes to it. This is why fork() followed by exec() (replace the child's program entirely) is fast even from a multi-gigabyte parent process, and why Redis can snapshot its in-memory dataset by forking and letting the child write out a frozen copy-on-write view while the parent keeps serving traffic.
What is a Thread?
A thread is an execution context inside a process: its own stack, register state, and program counter — and almost nothing else. Everything else is shared with the other threads of the process: the heap, global variables, code, and open file descriptors. When people say threads are "lightweight," this is the precise content of the claim: creating a thread allocates a stack (often around 8 MB of virtual address space on Linux, though only the touched pages become real memory) and a kernel bookkeeping structure, while creating a process duplicates an entire resource bundle.
The shared heap is simultaneously the whole appeal and the whole hazard. Threads communicate by simply reading and writing the same memory — no serialization, no copying, nanosecond-scale "message passing." But the same property means one thread writing through a stray pointer can corrupt state that every other thread depends on, and a crash in any thread kills the entire process. Isolation and efficiency are the two ends of one dial.
Context switch cost follows the same logic. Switching between threads of one process swaps registers and stack — the address space stays put. Switching between processes additionally changes the page table mappings and historically flushed the TLB (the translation lookaside buffer, the CPU's cache of virtual-to-physical mappings), after which memory access runs slow until the cache re-warms. Modern CPUs mitigate this with tagged TLB entries (PCID on x86), but the ordering survives: thread switches are cheaper than process switches, and the cheapest context switch is the one you don't make.
Key Differences at a Glance
| Process | Thread | |
|---|---|---|
| Address space | Private, MMU-enforced | Shared with sibling threads |
| Crash blast radius | Itself only | Entire process |
| Creation cost | Higher (mitigated by copy-on-write) | Lower (stack + kernel struct) |
| Communication | IPC: pipes, sockets, shared memory segments | Direct memory access |
| Context switch | Page table swap, TLB effects | Register/stack swap |
| Security boundary | Yes — usable for privilege separation | No |
The communication row deserves expansion, because it drives more architecture decisions than any other. Processes exchange data through inter-process communication (IPC): pipes and sockets (copied through the kernel, naturally serialized), or explicitly shared memory segments (mmap, shm_open) which recover thread-like speed at the price of reintroducing thread-like synchronization problems. Threads just use memory — which means their communication is fast by default and safe only by discipline.
The Hard Part: Sharing Correctly
Everything difficult about threads reduces to one fact: two threads touching the same data at the same time, with at least one writing, is a data race, and the result is undefined — not "one of the two values," but potentially torn writes, values out of thin air, and bugs that appear only under load, on certain hardware, every few million executions.
The standard tools each solve part of the problem and introduce another:
- Mutexes enforce mutual exclusion around critical sections. Held too broadly, they serialize your "concurrent" program (one giant lock means threads take turns); held too granularly, they invite deadlock.
- Deadlock has a precise recipe — the four Coffman conditions — but the practical version is: thread A holds lock 1 and wants lock 2, thread B holds lock 2 and wants lock 1, and both wait forever. The standard defense is a global lock ordering: all code acquires locks in one agreed sequence, making the circular wait impossible.
- Condition variables let threads sleep until a state change instead of spinning; atomics handle single-word updates (counters, flags) without a lock at all.
- Tooling matters more than cleverness. ThreadSanitizer and Go's built-in race detector find data races dynamically, and running them in CI is the single highest-leverage practice in concurrent codebases — races found by users are the most expensive kind.
The debugging pain is structural: a multithreaded program's behavior depends on the interleaving the scheduler happened to choose, so the failing execution may be unreproducible by construction. This is why experienced teams minimize shared mutable state architecturally — message-passing designs (Go channels, actor systems) and immutable data structures don't make races less harmful, they make them impossible to express.
One Language-Specific Landmine: The GIL
If you write Python, the theory above comes with an asterisk. CPython's Global Interpreter Lock allows only one thread to execute Python bytecode at a time — so threads give you concurrency (useful for I/O-bound work, where threads spend their time waiting on sockets and the GIL is released during blocking calls) but not parallelism (a CPU-bound workload on eight threads still uses one core). The idiomatic escape is the multiprocessing module — real processes, real parallelism, data exchanged by pickling through IPC — which is exactly the thread/process trade made for you by the language runtime. The rule of thumb: I/O-bound → threads (or async); CPU-bound → processes. (Recent CPython versions are experimenting with removing the GIL, but the rule of thumb still governs the installed base.)
It's also worth knowing that the industry has largely moved past raw OS threads for high-concurrency servers: Go's goroutines and Java's virtual threads multiplex enormous numbers of cheap user-space threads onto a small pool of OS threads, and event-loop runtimes (Node.js, Python asyncio, nginx internally) achieve high I/O concurrency on a single thread by never blocking at all. These are all responses to the same underlying fact — OS threads are cheap, but not free, and ten thousand of them is a scheduling and memory problem.
How Real Systems Choose
The trade-offs stop being abstract when you look at what production software actually does:
- Chrome runs each site in its own process, accepting significant memory overhead to get MMU-enforced isolation — a compromised or crashed tab cannot read another tab's memory. After the Spectre class of speculative-execution attacks, per-site process isolation went from defense-in-depth to the only trustworthy boundary, which is why every major browser adopted it.
- nginx runs a small set of worker processes (typically one per core), each running a single-threaded event loop handling thousands of connections. Processes give crash isolation and easy zero-downtime reloads; the event loop gives I/O concurrency without lock contention.
- PostgreSQL historically uses a process per connection — maximal isolation, at a per-connection memory cost that makes external connection poolers (PgBouncer) essentially mandatory at scale. MySQL uses a thread per connection: cheaper connections, shared buffer pool, one process to crash.
Notice the pattern: process boundaries appear wherever isolation or security is the requirement; threads (or lighter) appear wherever shared state and density are the requirement; and serious systems freely mix the two — processes for the coarse structure, threads or event loops inside each.
When to Use Which
- Use threads when tasks genuinely share state and communicate frequently — a web server's request handlers sharing a cache, a pipeline's stages passing large buffers. You accept synchronization discipline as the price of zero-copy communication.
- Use processes when you need fault isolation (one worker's crash must not kill the fleet), security boundaries (sandboxing untrusted code), or parallelism in a GIL-constrained runtime. You accept IPC overhead as the price of hardware-enforced walls.
- Reconsider both for massive-concurrency I/O workloads, where async runtimes and green threads dominate — and for CPU-bound data parallelism, where a work-stealing thread pool sized to the core count (not to the task count) is the standard answer.
Conclusion
Threads and processes are not two flavors of the same thing — they are two positions on a dial between sharing and isolation, and every property of each (cost, speed, failure modes, security) follows from that position. Processes buy you hardware-enforced boundaries and pay in memory, startup cost, and IPC overhead; threads buy you zero-copy communication and pay in synchronization discipline and shared blast radius. The systems you rely on every day — Chrome, nginx, PostgreSQL — each read those trade-offs against their own threat models and workloads, and reached different, equally defensible answers. That's the real lesson: the question is never "which is better," but "where, in this system, do I need walls — and where do I need doors."