Threads & Processes — Fundamentals¶
Process — the fundamental unit of isolation¶
A process is an instance of a running program, and the OS gives it its own isolated address space. Concretely, when the OS creates a process it sets up:
- Virtual memory space — its own page tables, so process A can't see or touch process B's memory. Enforced by the MMU (memory management unit) in hardware.
- Its own copy of code, data, heap, stack
- File descriptors / open handles
- Process Control Block (PCB) — kernel-side bookkeeping: PID, register state, scheduling info, memory map pointers, open file table, etc.
Because processes are isolated, communication between them requires explicit IPC (inter-process communication) — pipes, sockets, shared memory segments, message queues. Nothing is shared by default.
Creating a process is relatively expensive: fork() (on Unix) duplicates the parent's address space (copy-on-write these days, so it's cheaper than it sounds, but still involves new page tables, new PCB, etc.), and exec() then loads a new program image into it.
Thread — the fundamental unit of execution within a process¶
A thread is a sequence of instructions the CPU executes — it's what actually gets scheduled onto a CPU core. A process always has at least one thread (main thread), and can spawn more.
Key idea: threads within the same process share the address space — same heap, same global variables, same open file descriptors, same code segment. What each thread gets privately is:
- Its own stack (local variables, return addresses, function call frames)
- Its own register set / program counter — this is literally what defines "where execution currently is"
- Its own thread ID and scheduling state
That's it. Everything else — heap-allocated objects, globals, static data — is shared.
This has two big consequences:
-
Thread creation/switching is cheap. No new address space, no new page tables — just a new stack and register context. A context switch between threads in the same process just swaps register state and stack pointer; switching between processes also has to swap the memory mapping (page table base register), which is costlier — hence the general rule "threads are lighter than processes."
-
Shared memory means race conditions. Since threads see the same heap, two threads writing to the same variable concurrently is undefined/dangerous behavior unless you synchronize — hence mutexes, semaphores, condition variables, atomics. This is the entire reason concurrent programming is hard: isolation (process) is safe but expensive to communicate across; sharing (threads) is cheap to communicate but requires you to manually protect every shared mutation.
The scheduler's view¶
The OS scheduler doesn't really schedule "processes" — it schedules threads (in Linux, both are represented internally as task_struct; a "process" is just a thread group with a shared address space). Each core runs one thread at a time (ignoring hyperthreading), and the scheduler time-slices between all runnable threads across all processes, using a scheduling algorithm (e.g., Linux's CFS — Completely Fair Scheduler) to decide who runs next based on priority/fairness.
On a multi-core machine, true parallelism happens: different cores run different threads simultaneously. On a single core, you get concurrency via time-slicing (rapid switching creates the illusion of simultaneity) but not true parallelism.
Where Python complicates this¶
CPython has the GIL (Global Interpreter Lock) — only one thread can execute Python bytecode at a time, even on a multi-core machine. So:
- Threading in Python gives you concurrency, not parallelism, for CPU-bound work — useful for I/O-bound tasks (network calls, disk I/O) where a thread releases the GIL while waiting.
- CPU-bound parallel work in Python typically needs
multiprocessing(separate processes, separate GILs, separate memory — communicate via pickling over pipes/queues) or dropping to C-extensions that release the GIL (numpy, etc.). - This is exactly why Python microservices architectures often lean on process-level parallelism (multiple worker processes behind a load balancer, e.g. Gunicorn workers) rather than threading for CPU-bound throughput — sidesteps the GIL entirely, and you get the isolation as a bonus (one worker crashing doesn't take down others).
Quick mental model to keep¶
| Process | Thread | |
|---|---|---|
| Memory | Isolated | Shared (except stack/registers) |
| Creation cost | Expensive | Cheap |
| Context switch cost | Expensive (page table swap) | Cheap |
| Communication | Explicit IPC | Direct (shared memory) — needs locking |
| Crash isolation | One process crashing doesn't affect others | One thread crashing (segfault) can take down the whole process |
| Failure mode of concurrency bugs | Deadlock over IPC resources | Race conditions, deadlocks over shared state |