Skip to article
ALGORITHMICSConcurrency
Concurrency5 min read

Threads and Processes

What is shared decides everything else — cost, safety, and how a crash spreads.


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.

Unit of concurrency
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:

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.