Threadsprocessthreadisolationcontext switchIPC

Process versus Thread

Processes buy isolation at the price of expensive creation, expensive switching and explicit communication; threads buy cheap sharing at the price of shared failure — and every runtime, browser and database picks a point on that line for a reason.

ConceptualLinuxRuntime-specific
▶ InteractiveInterview question
Progress

The problem

Chrome runs each site in its own process; nginx runs a few worker processes each with an event loop; the JVM runs one process with hundreds of threads; PostgreSQL forks a process per connection while MySQL starts a thread. They are all right. What are they trading, and how do you choose?

The comparison

Linux

The table below is the whole lesson in one screen. Every row follows from one fact: threads share an address space and processes do not. Sharing makes communication and switching cheap and makes failure and corruption shared; separation makes them expensive and contained. Numbers are typical Linux x86-64 figures, order-of-magnitude only; measure on your platform before quoting them.

Process vs thread (Linux figures, order of magnitude)
DimensionProcessThread
Address spacePrivate; shares only read-only code pages and explicit shared memoryShared with all threads of the process
IsolationA wild pointer faults in its own process only; kernel enforces boundariesNone: any thread can corrupt any other’s data
CommunicationExplicit IPC: pipes, sockets, shared memory, message queues — copies or setup costPass a pointer; synchronise with mutexes/atomics
Failure impactA crash kills one process; a supervisor restarts itA crash (segfault, uncaught exception) kills the whole process
Switching cost~2–5 µs + page-table switch, TLB flush unless tagged (PCID/ASID), cold caches~1–2 µs, same page tables, TLB kept warm
Creation costfork ~50 µs–1 ms (scales with page-table size); exec adds loader time~10–50 µs; a stack mapping and a task record
Memory per unitFull private data + page tables (MBs); code sharedOne stack (8 MB virtual, few kB committed) + ~10 kB kernel state
Security boundaryYes: different uid, namespaces, seccomp possible per processNo: all threads run with the process’s credentials
Debug/observeOwn PID, own /proc entry, own limits, restartableTID under the process; shares limits; cannot be restarted alone

Isolation and the failure domain

Isolation is enforced by the MMU. A process that dereferences a bad pointer takes a page fault, the kernel finds no mapping, and sends SIGSEGV to *that* process. Its siblings never notice. That is why Chrome pays for a process per site: a renderer exploited by one page cannot read another page’s cookies from memory, and a renderer crash shows a sad tab, not a dead browser. It is why nginx and PostgreSQL survive a crashing worker with one reconnect rather than an outage.

Threads have no such wall. A segfault in one thread is a signal to the process; an uncaught exception terminates the process; a thread that corrupts the heap allocator’s metadata takes down whatever thread next calls malloc. Memory-safe languages remove the corruption but not the shared fate: an out-of-memory error or a stuck lock in one Java thread affects every thread in the JVM. The rule of thumb: put things that must not take each other down in separate processes, and things that must share large state cheaply in threads.

Communication and switching

Conceptual

Between threads, communication is a memory write plus a synchronisation primitive: a lock-protected queue, an atomic flag, a condition variable. Handing a 1 MB buffer to another thread costs a pointer. Between processes it costs a copy through the kernel (pipe, socket, message queue — see IPC: Deliberate Holes in Process Isolation) or the up-front work of mapping a shared-memory segment (Shared Memory: Zero Copies, Zero Protection) and then the *same* synchronisation problems as threads, because shared memory is shared memory.

Switching between two threads of the same process keeps the page tables and the TLB; switching between processes loads a new page-table root, which on x86 flushes the TLB unless the CPU tags entries with a process-context id (PCID, and ASIDs on ARM), and either way the new process starts with cold caches. The direct cost difference is small (a microsecond or so); the indirect cost — TLB misses and cache misses in the first milliseconds after the switch — is what shows up in benchmarks. See Context Switching.

  • Same-process switch: registers + stack pointer; TLB warm. Cross-process: also CR3/TTBR reload; TLB cold unless tagged.
  • Windows: switching between threads of different processes carries the same page-table cost; the thread is still the unit.

How real systems choose

Runtime-specific

Browsers: process per site for security isolation, threads inside each renderer for parsing, layout, JavaScript and compositing. nginx: one process per core, each running a single-threaded event loop, because the work is I/O-bound and the workers share nothing but the listening socket. PostgreSQL: process per connection, historically for robustness and portability, with explicit shared memory for the buffer pool — the reason PostgreSQL connections are expensive and everyone runs a pooler. MySQL, SQL Server, most JVM services: threads, because the buffer pool and caches are simply the heap.

Node.js: one process runs one event loop; to use eight cores you run eight processes (cluster) or worker threads with separate V8 isolates that share nothing by default. CPython: threads for I/O concurrency, processes for CPU parallelism because of the GIL in the default build — see How C++, JavaScript and Python Map onto the OS. Go, Java virtual threads, Erlang: one process, a few kernel threads, millions of cheap user-level tasks. None of these is the "right" answer; each is a position on the isolation-versus-sharing line chosen for its workload.

Key points

  • Every difference between processes and threads follows from whether the address space is shared.
  • Processes: isolation enforced by the MMU, explicit IPC, ~µs-to-ms creation, page-table switch on context switch, own credentials and limits.
  • Threads: pointer-passing communication, ~10–50 µs creation, cheaper switches with a warm TLB, shared credentials — and a crash or heap corruption in one kills all.
  • Put things that must not take each other down in separate processes; put things that share large mutable state in threads.
  • Chrome, nginx, PostgreSQL, MySQL, Node and Go each sit at a deliberate point on this line; the figures are platform-dependent — measure before quoting.

Why does this exist?

Mechanisms are answers to constraints. Open each question before reading the answer.

Why does a thread crash kill the whole process?

Because the kernel’s unit of protection is the address space; a corrupted heap or a fatal signal has no smaller container to be confined to.

Why is a process switch more expensive than a thread switch?

Switching the page-table root invalidates or re-tags the TLB and leaves the caches full of the old process’s lines; the extra direct cost is small, the indirect miss cost is not.

Why do databases disagree on this?

Workload and history: PostgreSQL chose per-connection processes for robustness on 1990s Unix and kept them; MySQL chose threads and a shared heap. Both then built the missing piece — shared memory for one, careful locking for the other.

Process or thread?

Process or thread?
Same seven questions, four workloads, different answers.
DimensionProcess Thread ✓
Address spaceOwn page tablesShared with siblings
IsolationStrong — kernel enforcedNone — same memory
CommunicationIPC: pipes, sockets, shmShared memory + locks
Failure impactOne dies, others liveOne crashes, all die
Switching cost~2–5 µs + TLB/cache flush~1–2 µs, same page tables
Creation cost~100 µs–1 ms (fork)~10–50 µs
Memory per unitMBs: page tables + private copiesKBs: stack + TLS
Recommendation: threads (or an event loop). Connections share one routing table and are mostly idle. Thousands of processes would waste page tables and make broadcast a serialisation problem. A thread pool — or better, an event loop with a few threads — is the fit.
Runtime-specificNode.js and Go use one event loop / goroutines; Java uses virtual threads; C++ uses a pool over epoll or io_uring.
Costs are typical orders of magnitude on Linux/x86, not measurements. On Linux both are `task_struct`s; a thread is a clone() that shares the address space.

How it fails

What the failure looks like from inside real software.

  • A multi-threaded service dies entirely because one request handler dereferenced null; the same bug in a process-per-worker design costs one request.
  • A PostgreSQL server with max_connections = 2000 runs out of memory: each connection is a process with its own private memory, not a thread with a small stack.
  • A Node service "uses only one core": one process, one event loop; the fix is cluster or worker threads, not more async.
  • Two processes share a memory segment and the developer assumes "processes are isolated, no locks needed" — the shared segment races exactly like threads do.
  • A thread pool sized to 200 on a 4-core box spends more time in context switches and cache misses than in work; throughput drops as threads are added.