A process and a thread look similar from userspace — both are "something that runs code" — but the kernel treats them as two different resources nailed together. A process is an address space with a scheduling unit attached. A thread is a scheduling unit with an address space attached. All the confusion between the two lives in that sentence.
A process is a bundle, not a thing
On Linux there is no separate "process" and "thread" kernel object. There is one structure,
task_struct, and a set of clone flags that decide what a new task shares with its creator.
fork() and pthread_create() are both implemented on top of the clone() syscall:
// fork() is roughly:
clone(SIGCHLD, 0);
// pthread_create() is roughly:
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD,
child_stack);CLONE_VM means share the address space — that flag is the entire difference. A process is
a task_struct that owns a fresh address space, file table, and signal dispositions. A thread
is a task_struct that borrowed them all from its creator.
fork: cheap because of copy-on-write
Forking used to be catastrophically expensive — the kernel literally copied every page of the
parent's memory. Modern Linux copies nothing. fork() does two things:
- Copies the page tables, marking every page read-only.
- Copies
task_structand the kernel stack.
That's it. The memory is shared; the read-only bit is a trap. The first time either process
writes a page, the CPU raises a page fault and the kernel duplicates just that one page.
Copy-on-write (COW) is why fork() of a 2 GB process is a few hundred microseconds instead
of seconds — the cost scales with the page table size, not the memory size, plus faults
amortized over the pages you actually modify.
clone: threads share the address space
A thread skips most of that. With CLONE_VM, there is no page table copy and no COW
machinery — the child shares the parent's address space directly. That's the cheaper part.
But it is not free, and the invoice shows up elsewhere:
- a new
task_struct(~2 KB) and a kernel stack (16 KB per thread on x86-64, virtually mapped) - a new thread ID (TID), scheduling entity, and run-queue bookkeeping
- FPU and signal machinery setup
Measured on modern x86: pthread_create() + pthread_join() costs roughly 10–50 µs,
while fork() + waitpid() on a modest process costs 50–500 µs, scaling with page-table
size. Threads are 5–10× cheaper — but "cheaper" is a long way from "free": at 10 µs per
thread, spawning 100,000 threads is a full second of pure setup.
What threads share (and what they don't)
Shared: the address space, open file descriptors, working directory, signal handlers, and the
PID namespace view. Not shared: register state (that is literally what a thread is to the
CPU), each thread's own stack, errno (a thread-local macro), the signal mask, and
scheduling priority.
The consequence that matters: threads have no fault isolation. A wild pointer in any thread segfaults the whole process; heap corruption in one thread can corrupt every other thread's allocations. Processes buy isolation — at the price of COW overhead and a TLB flush on every switch.
The fork-in-a-threaded-process trap
Multi-threaded fork() is a minefield: the child inherits only the calling thread. Every
mutex the other threads held is now locked forever in the child. That is why atfork
handlers exist and why a forked child must exec() immediately. If you fork in a
multithreaded program and never exec, you are gambling.
Choosing: an isolation question, not a speed question
| Process | Thread | |
|---|---|---|
| creation cost | 50–500 µs (page tables) | 10–50 µs |
| crash isolation | yes | no |
| switch cost | TLB flush + cache cold | same-address-space, cheaper |
| sharing data | IPC or shared memory | direct memory access |
Rule of thumb: 8 cores and 8 concurrent tasks → processes, cheaply. 10,000 concurrent network connections → threads, or better, user-space scheduling (goroutines, async runtimes) where you can. The kernel's 1:1 model gives every thread real parallelism but bills you per thread for state the kernel must maintain — state you may not need.