The Big Picture: Bookmarks
You are reading a book. A friend asks you to help with a puzzle. Before you leave, you put a bookmark in the book, and note where you were. When you come back, you open the book at the bookmark and carry on as if nothing happened.
A context switch is exactly that, done by the CPU. It stops thread A, saves where it was, loads where thread B was, and carries on with B. You met the idea in Chapter 2. Here we look at it from the thread's point of view.
core 0 time ──►
┌──────────────┬──────┬──────────────┬──────┬──────────────┐
│ Thread A │switch│ Thread B │switch│ Thread A │
│ running │ │ running │ │ running │
└──────────────┴──────┴──────────────┴──────┴──────────────┘
▲ ▲
save A's context save B's context
load B's context load A's context
The "switch" slices are pure overhead. No useful program work happens in them.
6.1 What Gets Saved and Restored
What it is: The context of a thread is everything the CPU needs to continue it exactly where it stopped. When a thread is paused, Windows saves its context into the thread's kernel data. When the thread runs again, Windows loads it back.
What is in a thread's context:
| Part | Why it is saved |
|---|---|
| Instruction pointer (also called program counter) | Which instruction to run next |
| Stack pointer | Where the thread's stack currently ends |
| General-purpose registers (RAX, RBX, ... on x64) | The thread's working values |
| Flags register | Results of recent comparisons (zero, carry...) |
| Floating-point and vector registers (XMM, YMM and so on) | For decimal maths and SIMD instructions. Saved when the thread used them, since they are large |
| Segment and control details | Per-thread settings, including a pointer to the thread's own data block (the TEB) |
Where it is saved: on the thread's kernel stack, plus fields in the thread's kernel object. Each thread has a small kernel stack for exactly this reason (and for running kernel code on its behalf).
What is NOT saved: the thread's memory contents. The stack and heap stay right where they are in RAM. Only the CPU's tiny working state moves.
Same process or different process? The kind of switch changes the cost:
| Switch type | What changes | Cost |
|---|---|---|
| Thread → thread, same process | Registers and stack only. The address space stays the same | Cheaper |
| Thread → thread, different process | Registers and stack plus the page-table pointer (CR3, which tells the CPU which process's memory map to use) | More expensive (Section 6.4) |
This is one real reason threads are "lighter" than processes.
6.2 Register State
What it is: Registers are the CPU's own tiny storage slots (Chapter 2). They are the fastest memory in the machine and hold what the thread is working on this instant.
The x64 register set, in plain groups:
| Group | Examples | Count (approx.) | Use |
|---|---|---|---|
| General-purpose | RAX, RBX, RCX, RDX, RSI, RDI, R8–R15 | 16 (counting RSP, RBP) | Numbers and addresses |
| Instruction pointer | RIP | 1 | Next instruction |
| Flags | RFLAGS | 1 | Status bits |
| Vector / floating-point | XMM0–15 (or YMM, ZMM on newer CPUs) | 16 or more, each 16–64 bytes wide | Decimal and SIMD maths |
A thread never knows it was switched out. The registers come back with the same values, so its code carries on as if nothing happened.
Quick example with an addition:
Thread A is in the middle of: total = total + 5
RAX = 100 (its total so far)
── switch to B ── (A's RAX = 100 saved in memory) ──
B uses RAX for its own work; RAX becomes 777.
── switch back to A ── (RAX is set to 100 again) ──
A continues: adds 5 → 105. Correct.
Lazy saving of heavy state. The vector and floating-point registers are big. Many threads never touch them. Modern CPUs and Windows therefore avoid saving or loading them unless needed.
6.3 Stack Pointer
What it is: The stack pointer (RSP on x64) is a register that holds the memory address of the top of the current thread's stack. A stack (Chapter 3) is the thread's notepad for function calls: each call pushes a new "frame" holding its local variables and where to return.
Each thread has its own stack, so each thread has its own call history. This is what lets thread A be deep inside ten nested function calls while thread B is just starting.
Thread A's stack Thread B's stack
(in the process's memory) (in the process's memory)
┌────────────────┐ ┌────────────────┐
│ main() │ │ WorkerMain() │
│ LoadFile() │ │ Compress() │
│ Parse() ◄─RSP A │ Crunch() ◄─RSP B
└────────────────┘ └────────────────┘
On a context switch the OS simply changes the stack pointer from A's stack to B's stack. After this one change, every "return" and every local variable the CPU touches belongs to B. That is why the stack pointer is the single most important item to swap.
Two stacks per thread. A Windows thread actually has:
| Stack | Used when | Size |
|---|---|---|
| User-mode stack | The program's code runs | Default reserve is about 1 MB per thread (set in the program) |
| Kernel-mode stack | Windows code runs for the thread (a system call, an interrupt) | Small and fixed (about 24 KB on 64-bit Windows) |
The kernel stack is protected, so a program cannot use it to attack the kernel. Context switches happen in kernel mode, so the saved registers live on the kernel stack.
Stack overflow: If a thread uses more stack than reserved (for example, a function that calls itself forever), the stack runs out and Windows ends the process with a stack overflow error. Each thread's stack is separate, so deep recursion in one thread cannot corrupt another's stack, although it can crash the whole process.
6.4 Context Switch Cost and Overhead
What it is: A switch is not free. The direct cost is small. The hidden cost is larger.
Direct cost (what the kernel does):
- Enter the kernel (timer interrupt or a wait call).
- Decide the next thread (scheduler code).
- Save registers; load the new ones.
- Switch stacks.
- Return to user mode.
This takes roughly a few microseconds (millionths of a second), or a few thousand CPU cycles. The exact figure varies a lot with the CPU and Windows version, so treat it as "about".
Hidden costs:
| Cost | What happens |
|---|---|
| Cold CPU caches | The new thread's data is not in the L1/L2 cache (Chapter 2). The first memory reads are slow until the cache refills. The old thread's data may have been pushed out |
| TLB flush | The TLB is a small cache of recent address translations (Chapter 3). When switching to a thread of a different process, the old translations are invalid. Modern CPUs tag entries (PCID) to avoid a full flush, but the cost is not zero |
| Branch predictor disruption | The CPU's guesses about where code will go next were learned for the old thread |
| Security protections | Some defences against CPU side-channel attacks add extra work around switches |
| Kernel time | All that time is "kernel time", not your program's useful work |
A useful 20 ms slice After a switch, the next slice starts slow
┌───────────────────┐ ┌─────────────────────┐
│███████████████████│ │░░░▒▒▒▓▓▓████████████│
└───────────────────┘ └─────────────────────┘
caches warm, full speed caches cold ► warming up ► full speed
What this means in practice:
- More threads than cores means more switching, and each switch wastes a little time. If you run 200 busy threads on 8 cores, a lot of time can be lost.
- Short quantum means more switches. Long quantum means fewer switches but less fair.
- Blocking often (many tiny waits) causes many voluntary switches.
- Good design: use about as many busy threads as you have logical processors, and let other work wait in a queue (thread pools, Section 9).
Measuring it: Performance Monitor has counters:
| Counter | Meaning |
|---|---|
\System\Context Switches/sec | Total switches per second across the whole PC |
\Thread(*)\Context Switches/sec | Per thread |
\Processor(_Total)\% Privileged Time | How much CPU time is spent in kernel mode |
A calm idle desktop often shows thousands of context switches per second. A busy server can show tens of thousands or more. There is no single "bad" number: compare against your own normal and watch for sudden jumps together with high CPU in kernel time.
Voluntary vs involuntary:
| Type | Cause |
|---|---|
| Voluntary | The thread chose to wait (I/O, lock, Sleep) |
| Involuntary | The OS forced it (quantum over, preempted by a higher priority) |
Many involuntary switches mean the CPU is overloaded. Many voluntary ones mean the program is waiting a lot.
Summary
| Term | Plain meaning |
|---|---|
| Context | The CPU state a thread needs to continue: registers, stack pointer, instruction pointer |
| Context switch | Save one thread's context, load another's |
| Saved where | The thread's kernel stack and kernel object |
| Not saved | The thread's stack and heap contents (they stay in RAM) |
| Stack pointer | Swapped so the CPU uses the new thread's stack |
| Same-process switch | Cheaper; no change of address space |
| Cross-process switch | Also changes the page-table pointer; more cost |
| Direct cost | A few microseconds of kernel work |
| Hidden cost | Cold caches, TLB and predictor effects |
\System\Context Switches/sec | Counter to watch the rate |