A process is a program with its own memory. A thread is a line of execution inside one.
One process can have many threads. They share the heap, the code, and every open file — and they each get their own stack and registers.
That single difference — shared or separate memory — determines everything else.
- memory
- shared — one address space
- creation cost
- ~10 µs, ~1 MB stack
- communication
- a variable
- a crash
- takes down every thread
- isolation
- none — any thread can corrupt any other
Cheap and fast because they share everything — which is also why every article in this track exists. Sharing is the feature and the hazard.
Why threads are cheap
Creating a process means new page tables, a fresh address space, and copying (or lazily sharing) the parent’s mappings. A millisecond, roughly.
Creating a thread means a stack and a scheduler entry. Ten microseconds, roughly, and about a megabyte of virtual address space for the stack — reserved, not committed, so the real cost is smaller than it looks.
Switching between threads of the same process is also cheaper, because the page tables and TLB stay valid. A process switch flushes them.
The GIL, and what it actually forbids
CPython and (historically) Ruby have a global interpreter lock: one thread executes bytecode at a time, even on 64 cores.
So Python threads give you concurrency and not parallelism — with one important exception. The GIL is released during I/O and inside C extensions like NumPy.
That makes the rule concrete:
- I/O-bound Python — threads work fine. Ten threads waiting on ten sockets genuinely overlap.
- CPU-bound Python — threads buy nothing. Use
multiprocessing, or NumPy, or another language.
Python 3.13 added an experimental free-threaded build without the GIL, and 3.14 is stabilising it. Worth watching; not yet the default.
The middle ground
The thread/process choice is not binary any more.
Virtual threads / goroutines / green threads. Scheduled in userspace, a few hundred bytes each. You can have a million, block them freely, and the runtime multiplexes them onto a handful of OS threads. Java 21, Go, and Erlang.
Fork with copy-on-write. fork() shares pages until one side writes. A
pre-fork server model — nginx, Gunicorn, Puma — gets process isolation with much
of a thread’s startup cost.
Shared memory between processes. mmap a region, or SharedArrayBuffer in
the browser. Isolation everywhere except one deliberate window, which is often
exactly the right shape.
What is shared, exactly
Within a process, threads share the heap, globals, file descriptors, and the current directory. They do not share the stack, registers, thread-local storage, or signal masks.
Two consequences worth knowing:
Thread-local storage is the cheapest fix for contention. A per-thread counter summed at the end needs no lock at all, and it also avoids false sharing if padded.
Shared file descriptors mean shared file offsets. Two threads writing to the
same descriptor interleave at the byte level. This is why concurrent logging to
one file needs a lock, or one writer, or O_APPEND with writes smaller than
PIPE_BUF.