A context switch is the OS pausing one running task and resuming another on the same core. It is what lets a few cores juggle hundreds of threads. Switching between threads of one process is cheap; switching between processes is much more expensive, and most of that cost is hidden in cold caches afterward. This explains why thread pools and async I/O are preferred over spawning lots of threads or processes.
What triggers a switch#
A switch happens either because the running task gives up the CPU or because the OS takes it away.
| Voluntary (task gives up the CPU) | Involuntary (OS takes it) |
|---|---|
sleep() |
Timer interrupt: time slice expired (often ~5–10 ms) |
Blocking I/O: read, recv, a DB call |
Higher-priority task becomes ready (preemption) |
wait() or yield() |
Hardware interrupt: disk done, packet arrived |
| Failing to acquire a mutex |
Every trigger follows the same flow:
- An interrupt or system call puts the CPU in kernel mode.
- The OS saves the current task's state to its TCB/PCB.
- The scheduler picks the next task.
- The OS loads the next task's state.
- The CPU returns to user mode.
- The next task resumes.
The user → kernel → user mode switch itself adds overhead: the CPU changes privilege level, saves extra state and checks permissions. Even a thread switch pays this.
Thread switch (same process)#
A thread switch only swaps CPU registers. These are saved to Thread 1's TCB, then loaded from Thread 2's:
| Register state | What it tracks |
|---|---|
| Program counter (PC) | Where in the code |
| Stack pointer (SP) | Where the stack is |
| General-purpose registers (RAX, RBX, RCX, RDX, ...) | Intermediate values |
| Floating-point / vector registers | Math in progress |
| Status/flags register | Comparison results, carry |
| Thread state | Running → Ready |
Unchanged, because both threads live in the same process:
| Stays the same | Why |
|---|---|
| Page table | Same memory mapping |
| TLB | Stays warm, no flush |
| Heap, code, files | Shared |
| Process PCB | Same |
This is a few hundred bytes of register state, roughly 1–10 µs including kernel entry and scheduling.
Process switch (different processes)#
A process switch does everything above, plus a change of address space:
- Save Process A's registers.
- Save A's memory context (the page table base register, CR3 on x86).
- Load Process B's page table base into CR3. Every virtual-to-physical translation changes.
- Invalidate the TLB entries that belonged to A. This is the expensive part.
- Load B's registers.
- Resume B.
Where the cost comes from#
Most of the cost is not the switch itself but what happens right after it.
-
Registers (~1 µs): fast; CPUs dump and reload register files quickly.
-
Page table switch (trivial): a single register write.
-
TLB misses (the real cost): the TLB caches recent address translations. After a switch it holds nothing useful for B, so B's first accesses must walk the page table in RAM.
A TLB hit costs ~1 ns; a miss costs ~100 ns (walk the page table, then cache the result). If B touches 200 distinct pages before the TLB is warm, that is 200 × 100 ns = 20 µs just on TLB misses.
-
Cache pollution: L1/L2/L3 caches are full of A's data. B misses until the caches warm up.
L1 hit L2 hit L3 hit RAM ~1 ns ~5 ns ~20 ns ~100 ns
Many modern CPUs tag TLB entries with an address-space ID (PCID on x86, ASID on ARM), so the OS doesn't always have to flush the whole TLB on a process switch. Entries for B still start cold, though, so the warm-up cost remains.
Thread vs process switch at a glance#
The numbers below are ballpark figures; they vary a lot by hardware and workload.
| Thread switch | Process switch | |
|---|---|---|
| Registers, SP, PC | Save and load | Save and load |
| Page table | No change | Swap (CR3) |
| TLB | Stays warm | Flushed or cold for the new process |
| CPU caches | Little impact (shared data) | Cold-start penalty |
| Typical cost | ~1–10 µs | ~50–100 µs including warm-up |
| Bottleneck | Register swap + scheduler | TLB and cache warming |
A process switch can be roughly 5–50× more expensive than a thread switch, and most of that is not the switch itself but the slow period afterward.
Why this matters when you design a server#
Compare two ways to serve many connections on a 4-core machine:
| 1,000 threads on 4 cores | Async I/O with 4 threads | |
|---|---|---|
| Threads waiting | ~996 at any moment | Each thread multiplexes many connections |
| Switching | Constant | Few switches between requests |
| Effect | A real share of CPU time goes to switching instead of working | A thread only yields when it is really waiting on I/O |
This is the reasoning behind thread pools (bound the number of threads) and async I/O (avoid one thread per connection). It also shapes CPU scheduling: a time slice that is too short spends most of its time switching.
Key takeaways#
- A context switch saves the current task's PC, SP and registers, then loads the next task's saved state.
- Thread switches inside one process keep the page table and TLB, so they are cheap (~1–10 µs).
- Process switches also change the page table, which leaves the TLB and caches cold; that warm-up dominates the cost.
- Every switch also pays a user → kernel → user mode transition.
- Fewer threads (pools) and non-blocking I/O (async) are the standard ways to cut switching overhead.