TRT
Sign in
Operating Systems

Processes, Threads, and the Cost of Sharing

A process is a protected resource container with an address space and operating-system-managed resources.

The Runtime Theory Team8 min read#processes#threads#scheduling

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.

c
#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

ProcessThread
Address spaceprivateshared with siblings
Crash takes downonly itselfthe whole process
Communicationpipes, sockets, shared memory — explicit and serializablereads and writes to shared variables
Startup costfork/exec, ~millisecondsstack + scheduling state, tens of microseconds
Safetyenforced by the kernelenforced 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 / BoundaryTypical LatencyDominant BottleneckProduction Mitigation
L1 / L2 Cache Access1 ns – 4 nsRegister dependency chains & pointer chasingContiguous memory layouts, cache-line alignment (64 B), and batch iteration
Main Memory (DRAM) Fetch60 ns – 100 nsTLB misses, random heap indirection, NUMA hopsHugePages (2 MB), arena/slab allocators, and pre-allocated ring buffers
Kernel Syscall / Context Switch1 µs – 5 µsUser-to-kernel mode switch, page-table flush, lock contentionNon-blocking readiness loops (epoll / io_uring), batching, and zero-copy I/O
NVMe SSD Random Read / fsync50 µs – 500 µsBlock I/O queue depth, write amplification, WAL flushesGroup commit, sequential append logs, and bounded buffer-pool pinning
Intra-Region RPC Roundtrip0.5 ms – 2.5 msSerialization, 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:

  1. 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.
  2. How is backpressure enforced when demand exceeds capacity? Unbounded in-memory queues, unthrottled retries, and blocking calls without explicit deadlines turn a localized 100 ms slowdown into a fleet-wide Out-Of-Memory (OOM) cascade. Always bound queue depths, propagate caller deadlines, and shed excess load early.
  3. 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.

Spotted a mistake or something broken? Report a problem

Not started

Sign in to save your learning progress.

Sign in to save