The Runtime Theory
RuntimeInternalsexecution

What happens when you spawn a thread?

A step-by-step walk from pthread_create to the clone syscall: the new task, its stack, the run queue, and the first context switch that runs it.

The Runtime Theory Team1 min read05 steps

layer stack

Runtime

HWHardware
KKernel
RTRuntime
APPApplication
SYSSystem
CLIClient
NETNetwork
TLSCrypto
SRVServer

adjacent altitudes in this subsystem are still being traced

trace spine

  1. 01 pthread_create builds the request
  2. 02 clone enters the kernel
  3. 03 Task and stack materialize
  4. 04 The scheduler queues the task
  5. 05 First run costs a context switch

new Thread(() -> work()).start() is the smallest unit of OS-level concurrency: the kernel creates a schedulable task that shares your address space but owns its own stack and register state. The whole act is one syscall plus a scheduler decision — here is what executes.

trace stepApplication

The runtime's pthread_create (glibc) prepares: it allocates the thread's stack — an 8 MB anonymous mmap by default (RSS grows on touch, as in malloc-trace's page fault) — and fills a pthread_attr with flags and stack pointer. It then calls clone with flags that say share the address space (CLONE_VM), share file descriptors, signal tables, and the PID namespace's thread group. All of this is user-space setup, ~microseconds.

trace stepKernel

clone is the syscall — the kernel's copy_process runs. Unlike fork, there is no address-space copy: the new task points at the parent's mm, and the kernel instead allocates a new task_struct (~several KB), a kernel stack, a thread_info, and a fresh set of register state. On x86-64 this is the clone syscall (or clone3 with extensible args), ~10–50 µs of kernel work — mostly slab allocations and scheduler bookkeeping, no I/O.

trace stepKernel

The new task is now a first-class scheduler entity: it has a state (NEW), a static priority, a struct sched_entity on the CFS run queue, and a thread group leader. Critically it does not start running yet. The kernel returns to the spawning thread, which continues executing immediately — start() returns before the child's first instruction has run. Spawn is asynchronous by construction.

trace stepKernel

The scheduler picks the task — either immediately, if a CPU has an idle run queue or an expired timeslice to preempt, or later, whenever the load balancer or the next schedule() call favors it. The child's first run begins at the task's entry point (the clone return path with a synthetic frame that calls the thread function — the kernel-set up stack already has the return address of start_thread).

trace stepHardware

The first run costs a context switch: the kernel saves the previous task's registers and stack pointer, restores the child's, and returns to user space in the child — with a different CR3-free address space (shared mm means no TLB flush, one of the big wins of threads over processes) and a cold cache. The child's first instructions are compulsory cache misses: its stack pages are cold, its code pages are likely shared with the parent, but its working set is empty. The switch itself is ~1–5 µs (pure register save/restore plus scheduling decision); the cold-cache penalty adds microseconds more.

bash
perf stat -e task:task_newtask,context-switches,page-faults -a sleep 0.1
# or time 10,000 spawns in your language of choice — typically 40–80 µs each

What the machine actually does is a two-step handoff: the kernel manufactures a full-fledged task — stack, register state, scheduler entry — in about 50 microseconds, then decides when it runs. The spawn is cheap and asynchronous; the scheduling is the part you don't control, and the first run is the part you pay for in cache misses.