What actually happens during a context switch, and why should an application developer care?
A context switch saves the running thread's registers and program counter, lets the kernel scheduler choose another runnable thread, and restores that thread's state; the dominant cost is usually the cache and TLB pollution afterwards rather than the save and restore itself.
What the interviewer is scoring
- Whether you can name the concrete state that is saved and restored, rather than saying "the CPU switches tasks"
- Whether you distinguish a thread switch inside one process from a full process switch, and can explain the address-space and TLB consequence
- Whether you recognise that the indirect cost of cache and TLB pollution usually dominates the direct cost
- Whether you separate voluntary from involuntary switches and know that they point at different root causes
- Whether you connect the mechanism to real advice such as thread-pool sizing, lock contention, or the existence of async I/O
Answer
The mechanics
A hardware thread executes one software thread at a time, so running more threads than the machine has hardware threads requires the kernel to multiplex them. (An SMT core exposes two hardware threads and retires instructions from both concurrently, which matters later: siblings share L1 and L2, so they pollute each other's cache without any switch occurring at all.) The unit of that multiplexing is the context switch, and the "context" is the processor state that makes a thread resumable: the general-purpose registers, the stack pointer, the program counter, the condition and status flags, and the extended register state used by floating-point and SIMD instructions. That extended state is much larger than the general-purpose registers on modern x86, which is why kernels avoid saving it for threads that never touched it.
The switch happens on entry to the kernel, either because the thread trapped in deliberately or because an interrupt pulled the core into kernel mode. The kernel copies the outgoing registers into that thread's kernel-side descriptor, calls the scheduler to pick the next thread, then loads the incoming thread's saved registers and returns to user mode. Restoring the program counter resumes the new thread, which from its own point of view simply took a very long time to execute one instruction.
The scheduler's role
Choosing the next thread is not free either. The scheduler maintains per-core run queues and decides which runnable thread has the strongest claim on the CPU, weighing priority, how much CPU it has already consumed relative to its fair share, and where it last ran. Linux used the Completely Fair Scheduler for many years and replaced its policy core with EEVDF in the 6.6 kernel; both approximate fairness while keeping the decision cheap enough to make thousands of times per second. Locality feeds into it too, which is why schedulers avoid migrating a thread to a core holding none of its data.
Thread switch versus process switch
Switching between two threads of the same process is the cheaper case. They share one address space, so the page tables do not change, the page-table base register is not reloaded, and the translation lookaside buffer stays valid. Only the register state and the kernel stack differ.
Switching between processes means switching address spaces. The kernel loads a new page-table root, and naively that invalidates every user-space TLB entry, because the same virtual address now means something entirely different. Hardware mitigates this by tagging entries with an address-space identifier — process-context identifiers on x86-64, ASIDs on ARM — so the old process's entries survive rather than being flushed. Tagging reduces the flush cost but does not remove contention for a finite TLB.
Why the indirect cost dominates
The direct cost is bounded and roughly fixed: a known amount of register shuffling plus a scheduler decision. The indirect cost is not. When the new thread starts running the caches are full of the old thread's data, so every access to its own working set is a miss refilled from a lower cache level or from memory, and the misses continue until that working set is resident again. Meanwhile it has evicted the old thread's lines, so the old thread pays the same penalty when it returns; branch predictors and prefetchers are cold too.
That indirect cost can exceed the direct cost by an order of magnitude or more, and it scales with how large and how different the two working sets are. Two threads sharing one small hot loop switch almost for free; two threads each streaming a large buffer destroy each other's cache residency every time they alternate.
Voluntary versus involuntary
A voluntary switch happens because the thread cannot continue: it blocked on a read, a contended lock, a sleep, or a condition variable. An involuntary switch happens because the kernel took the CPU away, either because the time slice expired or because a higher-priority thread became runnable. Linux exposes both counts, and the ratio is diagnostic. Read them for the process you are investigating rather than for the shell command you just ran, and remember the counters are per-task, so a thread breakdown means reading them under task/.
# High voluntary count: the thread keeps blocking - I/O or lock contention.
# High nonvoluntary count: too many runnable threads for the cores available.
grep ctxt_switches /proc/<pid>/status
# Per-thread, since the counters are maintained per task, not per process.
grep ctxt_switches /proc/<pid>/task/*/status
A high voluntary count points you at what the thread is waiting for; a high involuntary count points at oversubscription, which no amount of tuning the blocking code will fix.
Where this shows up in real advice
This is the mechanism behind guidance that otherwise sounds arbitrary. A thread pool sized far above the core count does not raise throughput for CPU-bound work; it only adds runnable threads competing for the same cores, so each is preempted more often and each working set is repeatedly evicted by the others. Lock contention is expensive largely because a blocking lock turns a very short critical section into a park-and-wake pair of switches, which is why many mutex implementations spin briefly before parking — the bet is that the lock will be released faster than a switch would cost. And non-blocking I/O exists so that waiting on a socket does not require dedicating a thread to the wait: a few threads stay resident and cache-warm while thousands of operations are in flight.
The trap
The trap is answering only the mechanical half. Most candidates list what gets saved and restored and stop, which reads as memorised. The strong answer says the save and restore is the small, predictable part and that the real cost is the cache and TLB state the incoming thread does not have, then uses that to explain why an oversized pool hurts or why async I/O earns its complexity.
The saved registers are the cheap, visible part of a context switch; the expensive part is the cache, TLB and branch-predictor state the incoming thread does not have, which is why reducing switch frequency matters far more than making each switch faster.
Likely follow-ups
- Why does an oversized thread pool often make a CPU-bound service slower rather than faster?
- How does a blocking mutex differ from a spinlock in terms of context switches, and when is spinning the better choice?
- What does the operating system have to do differently when a thread is rescheduled onto a different physical core?
- How would you tell from production metrics whether a service is suffering from involuntary preemption or from blocking on I/O?
Related questions
- How would you design a thread-safe component, and why is adding synchronized to every method not a design?hardAlso on concurrency7 min
- What does the GIL actually prevent, and how do you decide between threads, processes, and asyncio?mediumAlso on concurrency4 min
- Field data says your page has an INP of 400 ms. How do you find the cause and fix it?hardAlso on scheduling4 min
- What problem do virtual threads actually solve, and when do they not help?mediumAlso on concurrency5 min
- Several producers write to one channel and one consumer reads it. Who closes the channel?mediumAlso on concurrency4 min
- Merge a list of overlapping intervals, and justify the key you sorted byeasyAlso on scheduling4 min
- A family plan shares 100 GB across five SIMs with each SIM capped at 30 GB. How does charging enforce both limits at once?hardAlso on concurrency6 min
- How do you turn an effort estimate into a delivery date you would defend, and what do you do when the sales lead has already promised an earlier one?hardAlso on scheduling5 min