A process is a protected resource container with an address space plus operating-system-managed resources: file descriptors, credentials, working directory, signal handlers. A thread is an execution stream inside that container: its own program counter, register set and stack, sharing everything else. The split decides what communication costs — threads share for free and must synchronize; processes pay to communicate and get isolation for free.
What each thread actually gets
Creating a thread inside an existing process is cheap because nothing is duplicated: the address space, mappings, and open descriptors are shared. The kernel only builds scheduling state and a stack. The stack is the expensive-looking part — Linux gives a pthread 8 MB of virtual space (ulimit -s sets it), committed page by page as it is used, so a small thread really touches ~16–64 KB. Ten thousand threads therefore reserve 80 GB of address space (fine on 64-bit) but commit only a few GB of RAM — until stack-heavy code or a deep recursion turns that reservation into real pages and a real OOM.
#include <pthread.h>
#include <stdio.h>
long counter = 0; // shared heap — no synchronization
void *bump(void *arg) {
for (int i = 0; i < 10000000; i++) counter++; // race: load, add, store
return NULL;
}
int main(void) {
pthread_t a, b;
pthread_create(&a, NULL, bump, NULL);
pthread_create(&b, NULL, bump, NULL);
pthread_join(a, NULL); pthread_join(b, NULL);
printf("%ld (expected 20000000)\n", counter); // often 13-19 million
}Two threads incrementing the same word do not produce 20,000,000: the loop compiles to a load, an add and a store, and interleavings lose updates. The fix is a mutex — or an atomic increment, which on x86 compiles to a single lock add and still costs ~20 cycles when other cores contend for the line, versus ~4 cycles uncontended. Sharing is free to set up and expensive to get right.
Scheduling and blocking
When a thread blocks on a socket or a lock, the kernel parks it and runs another runnable thread — one thread's wait does not idle the process. Context switching saves registers and switches stacks: on the order of 1–10 microseconds including scheduler work, plus TLB effects when switching between processes (each process has its own address space, so some translation entries are flushed). That cost is why a thread-per-connection server degrades with idle clients: 50,000 connections means 50,000 stacks, 50,000 scheduling entities, and a run queue the scheduler walks, even though nearly all of them are asleep in epoll. Event-loop or pooled models collapse those waiters into a handful of threads.
Isolation versus sharing, in practice
| Process | Thread | |
|---|---|---|
| Address space | private | shared with siblings |
| Crash takes down | only itself | the whole process |
| Communication | pipes, sockets, shared memory — explicit and serializable | reads and writes to shared variables |
| Startup cost | fork/exec, ~milliseconds | stack + scheduling state, tens of microseconds |
| Safety | enforced by the kernel | enforced by your locking |
The choice follows from failure and communication needs. Run plugins, untrusted code, or anything that must not corrupt your heap in separate processes (this is what a browser's site isolation and a Python worker pool do). Keep tightly-coupled, low-latency cooperating loops as threads where a lock or a lock-free structure is cheaper than a syscall. Modern hybrids blur it: Go multiplexes goroutines onto OS threads, Erlang runs processes with cheap mailboxes — the lesson survives, which is that shared mutable memory is a design decision, not a default.
Key takeaways
- Threads share the address space by default: communication is one memory write, and correctness becomes your problem (the lost-update counter above is the minimal example).
- A thread's 8 MB stack is virtual until touched — overcommitment hides inside idle threads and shows up as a sudden memory wall at load.
- Blocking a thread is cheap for the OS but not for your thread count; thousands of mostly-sleeping threads are a memory and scheduler problem, not a parallelism win.
- Choose processes when you need crash isolation and explicit IPC, threads when you need shared state and low-latency coordination — and know that a process switch costs slightly more than a thread switch because address spaces change.
Production cost model and latency numbers
To apply Processes, Threads, and the Cost of Sharing effectively in production systems, engineers must move beyond qualitative diagrams and quantify the exact hardware and network costs at each boundary:
| Operation / Boundary | Typical Latency | Dominant Bottleneck | Production Mitigation |
|---|---|---|---|
| L1 / L2 Cache Access | 1 ns – 4 ns | Register dependency chains & pointer chasing | Contiguous memory layouts, cache-line alignment (64 B), and batch iteration |
| Main Memory (DRAM) Fetch | 60 ns – 100 ns | TLB misses, random heap indirection, NUMA hops | HugePages (2 MB), arena/slab allocators, and pre-allocated ring buffers |
| Kernel Syscall / Context Switch | 1 µs – 5 µs | User-to-kernel mode switch, page-table flush, lock contention | Non-blocking readiness loops (epoll / io_uring), batching, and zero-copy I/O |
| NVMe SSD Random Read / fsync | 50 µs – 500 µs | Block I/O queue depth, write amplification, WAL flushes | Group commit, sequential append logs, and bounded buffer-pool pinning |
| Intra-Region RPC Roundtrip | 0.5 ms – 2.5 ms | Serialization, TCP/TLS framing, tail queueing ($L = \lambda W$) | Connection pooling, deadline propagation, and p95 hedged requests |
Failure modes, debugging, and operational checklist
In real incidents, systems rarely fail at average (p50) load—they fail when a slow dependency, skewed hot key, or resource leak pushes concurrency across a non-linear saturation cliff. When reviewing code or architecture that relies on Processes, Threads, and the Cost of Sharing, walk through three verification questions:
- What is the invariant under partial failure or contention? If a worker crashes mid-operation, a network packet is delayed and reordered, or two threads race on the same state transition, prove which atomic primitive, version number, or idempotency key prevents silent corruption.
- How is backpressure enforced when demand exceeds capacity? Unbounded in-memory queues, unthrottled retries, and blocking calls without explicit deadlines turn a localized
100 msslowdown into a fleet-wide Out-Of-Memory (OOM) cascade. Always bound queue depths, propagate caller deadlines, and shed excess load early. - Which metrics and traces isolate the bottleneck in under 60 seconds? Instrument saturation, queue wait time vs service execution time, cache hit/miss ratios, and error rates by failure reason so on-call engineers can immediately distinguish a compute bottleneck from an I/O or lock-contention stall.