The Runtime Theory
Operating Systems

Process vs Thread: What's Actually Different

Why fork and clone are the same syscall, why threads are 5-10x cheaper to create than processes, and what you give up in isolation when you share an address space.

The Runtime Theory Team3 min read#processes#threads#fork#clone#kernel
On this page

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:

c
// 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:

  1. Copies the page tables, marking every page read-only.
  2. Copies task_struct and 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

ProcessThread
creation cost50–500 µs (page tables)10–50 µs
crash isolationyesno
switch costTLB flush + cache coldsame-address-space, cheaper
sharing dataIPC or shared memorydirect 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.