Threadsthreadstackregistersshared memorythread-local storage

Threads: Several Instruction Streams in One Process

A thread is an independently scheduled instruction stream inside a process: it has its own stack, registers and instruction pointer, and shares everything else — heap, globals, descriptors — with its siblings, which makes threads cheap to create and communicate through, and easy to corrupt.

ConceptualLinuxC++
▶ InteractiveInterview question
Progress

The problem

A server has one address space full of state — the connection table, the cache, the config — and wants to handle eight requests on eight cores at once. Eight processes would each need a copy of that state and a way to share updates. What is the cheapest unit the OS can schedule that still sees the same memory?

One process, three threads

Split what a process owns into two piles. The execution pile: an instruction pointer, a stack pointer, the other registers, a stack, a scheduling state, a signal mask, a small block of thread-local storage. The environment pile: the address space, the heap, the globals, the code, the descriptor table, the working directory, the uid, the signal handlers. A thread is one execution pile. A process is one environment pile plus at least one thread. Creating a second thread means creating a second execution pile in the same environment.

The scheduler schedules threads, not processes. On Linux this is literal: the kernel’s unit is the task (task_struct), a "process" is a group of tasks sharing an mm_struct and a thread group id, and ps -T or /proc/<pid>/task/ shows each one with its own TID. The main thread’s TID equals the PID. On Windows, a process is a container object and threads are the schedulable objects inside it — the same split under different names.

What is shared and what is private
read/writeread/writeProcess: address space · heap · globals · descriptors · uidThread A: stack, registers, IP, TLSThread B: stack, registers, IP, TLSThread C: stack, registers, IP, TLSShared heap object
UserLLMAgentToolDataDecisionHumanGuardrail

Shared versus private

Sharing the address space is the feature and the hazard. A pointer created by thread A is valid in thread B with no translation, so passing a 100 MB structure between threads costs one pointer copy. But the same pointer means B can write the structure while A reads it, and nothing in the hardware prevents that: the two threads are simply two instruction streams touching the same bytes. Every rule about mutexes, atomics and memory ordering exists because of this one design decision — see Race Conditions and Mutexes.

Each thread’s stack is private by allocation, not by protection. It is an mmap’d region in the shared address space (Linux glibc reserves 8 MB of virtual space per thread by default, Windows 1 MB; the pages are committed on touch), so thread B *can* read or corrupt thread A’s locals if it has a pointer to them. Returning a pointer to a stack variable and handing it to another thread is undefined behaviour precisely because that stack frame is gone the moment the function returns. Thread-local storage (thread_local in C++, threading.local in Python, AsyncLocalStorage is the async analogue in Node) gives each thread its own copy of a named global, implemented as an offset from a per-thread base register.

Per-thread vs per-process
ItemPer threadShared by all threads
Instruction pointer, registers, flagsyes
Stack (locals, return addresses)yes (private by convention, not by protection)
Scheduling state, priority, CPU affinityyes
Signal mask, errno, thread-local storageyes
Heap, globals, static data, codeyes
Descriptor table, working directory, uid/gidyes
Signal handlers, address space limitsyes

Kernel threads and user threads

Conceptual

A kernel thread (1:1 model) is one the kernel knows about and schedules directly: pthread_create on Linux calls clone() with CLONE_VM | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD, and the result is a task the scheduler can put on any core. Every mainstream runtime — C++ std::thread, Java platform threads, Python threading, Rust std::thread, Node worker threads — uses this model. The costs are the kernel’s: creation ~10–50 µs, a context switch ~1–5 µs, and a kilobyte-scale kernel stack plus the user stack per thread.

User threads (M:N or green threads) are scheduled by the runtime on top of a few kernel threads: Go goroutines, Erlang processes, Java virtual threads, and in a degenerate form every async task in an event loop. They start with a tiny stack (Go: a few kB, grown on demand), switch in tens of nanoseconds, and can number in the millions — but the kernel cannot see them, so a blocking syscall in one would block the kernel thread carrying it and every other user thread on it. Runtimes solve that by intercepting blocking calls and parking the user thread instead (Go’s netpoller, Java’s virtual-thread I/O integration). The distinction matters when you count: "10,000 threads" is a big number for kernel threads and a small one for goroutines.

Creating and joining

C++

The API is the same shape everywhere: start a function on a new thread, later join it (block until it finishes) or detach it (let it run and clean itself up). A joinable thread that is never joined leaks its stack and record; C++ terminates the program if a joinable std::thread is destroyed. A thread that throws or segfaults takes the whole process down — a signal is delivered to the process, and an uncaught exception in a std::thread calls std::terminate. There is no "restart this thread" the way a supervisor restarts a process, which is one of the arguments for process isolation covered in Process versus Thread.

Two threads sharing a counter — the canonical bug, already visible
1#include <thread>
2#include <iostream>
3
4int counter = 0; // shared: lives in .bss, one copy per process
5
6void work() {
7 for (int i = 0; i < 1'000'000; ++i) ++counter; // read-modify-write, not atomic
8}
9
10int main() {
11 std::thread a(work), b(work); // two kernel threads via clone()
12 a.join(); b.join();
13 std::cout << counter << '\n'; // almost never 2000000
14}

Key points

  • A thread is an execution context (IP, registers, stack, scheduling state, TLS); a process is an environment (address space, heap, descriptors, credentials) plus one or more threads.
  • Threads share the heap, globals, code and descriptor table; they have private stacks and registers — private by convention, not by hardware protection.
  • The scheduler schedules threads; on Linux each thread is a task with its own TID and the main thread’s TID is the PID.
  • Kernel threads (1:1) cost ~10–50 µs to create and ~MB of virtual stack; user threads (M:N, goroutines, virtual threads) cost kilobytes and cannot block the kernel thread carrying them.
  • A crash in any thread kills the process; a joinable thread must be joined or detached.

Why does this exist?

Mechanisms are answers to constraints. Open each question before reading the answer.

Why have threads when processes already exist?

Because sharing an address space makes communication a pointer copy and creation ten times cheaper than fork — the price is that nothing protects one thread’s data from another.

Why does each thread need its own stack?

A stack records the chain of calls in progress; two instruction streams have two different chains, and interleaving them on one stack would corrupt both.

Why do user-level threads exist on top of kernel threads?

To make the unit of concurrency cheaper than the kernel can: kilobyte stacks and nanosecond switches, at the cost of the runtime having to manage blocking itself.

Threads inside a process

Threads inside a process
A thread is an execution path. A process is the container: address space, descriptors, identity.
Process · PID 4102 · ./serverone address space
Shared by every thread
Code (text)
r-x, one copy
Globals / data
rw-, visible to all
Heap
malloc / new / objects
FD table
sockets, files
Signal handlers, cwd, uid
process attributes
Per thread (private)
Why threads are cheap. Creating a thread allocates a stack (a few hundred KB to 8 MB reserved, mostly untouched) and a kernel task entry — no new page tables, no copied descriptors. Sharing the heap means passing a pointer instead of serialising data.
Why threads are dangerous. A segfault, an uncaught exception, or an abort() in T0 kills the whole process — every other thread dies mid-operation. A single thread writing a shared variable without a lock corrupts state for all of them. Isolation is what you gave up.
2 threads · click a thread to highlight its stack

How it fails

What the failure looks like from inside real software.

  • A counter incremented by two threads ends up short: the read-modify-write interleaves — Race Conditions.
  • A thread returns a pointer to a local and another thread reads garbage or crashes: the frame was popped from the private stack.
  • Creating 5,000 threads on a 32-bit build, or in a container with a low pids cgroup limit, fails with EAGAIN from pthread_create long before RAM runs out.
  • An exception escapes a worker thread and the whole server dies with terminate called after throwing …, taking every other request with it.
  • A thread-local cache is filled per thread, so a 64-thread pool holds 64 copies and "the cache" uses 64× the expected memory.