AlgoMaster Logo

Context Switching

16 min readUpdated August 7, 2026
Listen to this chapter
Unlock Audio

A logical CPU executes one instruction stream at a time, yet a laptop may keep a browser responsive while compiling code, playing music, and running a database.

The operating system creates this illusion of simultaneous progress by repeatedly changing which thread owns each CPU. The mechanism that makes each suspended computation continue from exactly where it stopped is a context switch.

A context switch saves the execution state of one thread and restores the saved state of another so the CPU can resume the second thread.

Context switching is essential for multitasking. It is also not free. The kernel performs bookkeeping during the switch, and the incoming thread may find that useful CPU cache and translation state have grown cold.

What “Context” Means

At any instant, a CPU contains the immediate state of the thread it is executing. That state includes values such as:

  • The instruction pointer, which identifies the next instruction
  • The stack pointer
  • General-purpose CPU registers
  • Status and control registers
  • Floating-point and vector-register state when applicable
  • Architecture-specific thread state

Collectively, these values answer a practical question:

If this thread stops now, what must be preserved so it can later continue as though nothing happened?

The kernel also tracks scheduling state, accounting information, the thread's kernel stack, and references to resources such as its address space. Some of this information lives directly in the task's kernel bookkeeping; some is saved on its kernel stack or in architecture-specific structures.

A context switch does not normally copy the process's heap, executable code, or files. Those are longer-lived resources. The switch changes which saved execution context is active on the CPU.

There is only one set of real registers. A context switch is the work of emptying them into one saved area and filling them from another.

Only one of these contexts is active on a particular logical CPU at a given instant.

A Context Switch Step by Step

Consider one CPU running thread A while thread B is ready to run.

Conceptually, the transition proceeds as follows:

  1. Control enters the kernel because A blocks, yields, or is preempted.
  2. The kernel records A's execution state and updates its scheduling state.
  3. The scheduler selects B from the runnable work for that CPU.
  4. The kernel changes to B's kernel stack and scheduling context.
  5. If A and B use different address spaces, the memory-management context is changed as required.
  6. B's saved CPU state is restored.
  7. Execution resumes at B's saved instruction pointer.
  1. Thread A is running.
  2. Execution enters the kernel.
  3. A's context is saved.
  4. The scheduler chooses B.
  5. The task and address-space context are changed.
  6. B's context is restored.
  7. Thread B is running.

The exact sequence depends on the processor architecture and kernel implementation. For example, entering the kernel may already have saved part of the outgoing register state before the scheduler runs. The important guarantee is that every user-visible value required for correct continuation is preserved.

When A is chosen again, it does not restart its function. Its instruction pointer, stack, and registers are restored, so it continues after the point at which it stopped.

Thread Switching by the Scheduler

Programs are organized as processes, but schedulers work with independently executable threads.

A single-threaded process contributes one schedulable thread. A multithreaded process contributes several. On Linux, each of these schedulable entities is represented as a task.

This leads to precise terminology:

  • A switch between threads in different processes is a context switch.
  • A switch between two threads in the same process is also a context switch.
  • On a multicore machine, two threads can run simultaneously on different CPUs; no switch between them is required.

The phrase process context switch is commonly used when the outgoing and incoming threads belong to different processes. That case can require more memory-management work, but the fundamental scheduler operation still switches execution between threads.

What Causes a Context Switch?

Context switches happen when the current thread stops owning a CPU and another runnable thread is selected.

There are two broad categories.

Voluntary context switches

A voluntary switch occurs when the running thread gives up the CPU because it cannot or does not want to continue immediately.

Common causes include:

  • Waiting for network or file input
  • Sleeping until a timer expires
  • Waiting for a child process
  • Blocking on a synchronization primitive
  • Explicitly yielding the CPU

For example, if a server performs a blocking read and no data is available, keeping that server on the CPU would accomplish nothing. The kernel marks it as waiting and runs something else.

The wakeup makes the server eligible to run again. A later scheduler decision restores its context.

Involuntary context switches

An involuntary switch occurs when the thread remains capable of running but the kernel takes the CPU away.

This can happen because:

  • Its current CPU-time allocation has ended
  • More urgent runnable work should execute
  • The scheduler needs to distribute CPU time among competing tasks

The outgoing thread normally remains runnable. It did not request a blocking operation; it is waiting for another turn on a CPU.

The words voluntary and involuntary describe why the task stopped running. They do not imply that one category is good and the other is necessarily bad.

Blocking, Preemption, and the State Model

A context switch often accompanies a state transition, but the two ideas are not identical.

When a running thread blocks, it moves from running to waiting, and the scheduler usually switches to another runnable thread.

When a running thread is preempted, it moves from running to ready. It remains eligible to execute, but a different thread becomes active.

When a waiting thread's event completes, it moves from waiting to ready. The wakeup alone does not prove that a context switch occurs immediately. The current thread may continue, or the newly runnable thread may be selected, depending on the scheduler's decision.

Similarly, if the only runnable thread voluntarily yields, the kernel may have no different thread to select. A scheduling decision does not always result in a switch to a different task.

Mode Switches vs. Context Switches

A mode switch changes the CPU's privilege level, usually between user mode and kernel mode. A context switch changes the executing thread.

Compare a system call that completes immediately with a blocking read:

StageImmediate operationBlocking operation
StartUser mode, thread AUser mode, thread A
EntryA system callA blocking read
In the kernelKernel mode, thread AKernel mode, thread A
What happens nextThe result is readyA must wait
EndUser mode, thread AThe kernel switches A to B, and thread B resumes

In the first path, the CPU entered the kernel and returned to user mode while thread A remained current. This is a mode switch without a task context switch. The second path includes both a mode transition and a context switch.

Hardware interrupts also transfer control to the kernel, but an interrupt does not automatically mean another thread will run. The kernel may handle the event and return to the interrupted thread. A context switch happens only if scheduling selects a different thread.

This distinction matters when interpreting measurements: counting system calls or interrupts is not the same as counting context switches.

Switching Between Threads and Processes

The outgoing and incoming threads determine how much surrounding state can remain useful.

Threads in the same process

Threads in one process share an address space. A switch between them must change the active register values, stack pointer, instruction pointer, and thread-specific state, but it can keep the same process memory map active.

Each thread still has a different user stack and execution history. Shared memory does not mean shared register state.

Threads in different processes

Different processes normally use different virtual address spaces. The kernel must activate the incoming process's memory-management context in addition to changing the execution context.

Modern processors provide address-space tags, often called ASIDs or PCIDs, that can allow translation entries from multiple address spaces to coexist. Consequently, a process switch does not necessarily flush every translation from the translation lookaside buffer.

The exact cost varies by processor and operating system. It is safer to say that switching address spaces can require additional work and can reduce translation locality—not that every process switch always empties the entire TLB.

SwitchExecution state changes?Address-space change normally required?
Between threads in one processYesNo
Between threads in different processesYesYes

Both switches can disturb CPU-local state even when their direct kernel work differs.

The Direct Cost of a Context Switch

During a context switch, the CPU is executing kernel bookkeeping rather than the application work that either thread exists to perform.

Direct work may include:

  • Saving and restoring register state
  • Updating scheduler and accounting data
  • Switching kernel stacks
  • Changing memory-management context
  • Applying architecture-specific state changes

This cost is real, but it is only part of the performance story. A raw count of context switches does not reveal how expensive each one was, and there is no universal duration that applies to all machines and workloads.

The cost depends on the processor, kernel, security mitigations, relationship between the two tasks, and state that must be changed.

The Indirect Cost: Losing Warm CPU State

The larger cost can appear after the switch.

While thread A runs, it builds a useful working set in CPU structures:

  • Recently used instructions and data occupy caches.
  • Virtual-to-physical translations occupy the TLB.
  • The processor learns branch behavior.

When thread B runs, it uses those same finite hardware resources. B's working set can displace parts of A's. When A eventually resumes, it may suffer more cache and TLB misses while its useful state becomes warm again.

The operating system does not normally erase the CPU caches on every context switch. Cache contents are simply reused and replaced according to normal hardware behavior. This distinction is important: the penalty comes from interference and lost locality, not from a mandatory full cache flush.

A switch between threads that touch the same shared data may preserve more useful cache state than a switch between unrelated, memory-intensive processes. Conversely, two threads in one process can still have entirely different working sets and interfere heavily.

Loading simulation...

Locality Costs of CPU Migration

A context switch occurs on a logical CPU: one task stops running there and another starts.

A thread may later resume on a different CPU. This is a CPU migration. For example, a thread that previously ran on CPU 0 may later resume on CPU 3.

The new CPU may not have A's recently used data in its local caches. Some cache levels may be shared across cores, while others are private, so the effect depends on the hardware topology.

Schedulers balance two competing goals:

  • Keep work near warm CPU-local state
  • Move work when necessary to balance load and meet scheduling needs

CPU affinity can restrict where a thread runs and sometimes improve locality, but it can also prevent useful load balancing. It should be applied only after measurement shows that migration is part of a real bottleneck.

Switch Time vs. Off-CPU Time

Suppose a request-handling thread is preempted:

Thread A stops, a context switch runs, B and C run for a while, another context switch runs, and A resumes. Only the two switches are kernel work.

The context-switch operations themselves occupy only the small transition regions. The time during which B and C run is off-CPU time for A.

For latency-sensitive services, the scheduling delay before A runs again may matter more than the mechanical cost of saving and restoring registers.

This distinction prevents a common diagnostic mistake:

  • A high request latency is not proof that individual switches are slow.
  • The thread may instead be runnable but waiting behind other CPU work.
  • It may also be blocked on I/O or synchronization, which is a different form of off-CPU time.

Good performance analysis asks both how often a task switches and why it remains off CPU.

Measuring Context Switches on Linux

Linux exposes cumulative switch counts for each task.

For a single-threaded process or the thread-group leader:

Example fields look like:

In a multithreaded process, inspect a particular thread through:

Do not assume /proc/PID/status is a sum across every thread. Per-thread inspection is often necessary to find the worker producing the switches.

The pidstat utility can report rates over time:

Its cswch/s column reports voluntary switches per second, while nvcswch/s reports involuntary switches per second. Adding thread-level reporting can expose imbalance hidden by a process-wide view:

For one command, Linux perf can count switches and CPU migrations:

Availability and permissions vary by system. These tools count events; they do not by themselves explain which resource, lock, or competing task caused each switch.

A Runnable Context-Switch Experiment

Linux also exposes context-switch counts through getrusage(). The following program compares a CPU-bound workload with a workload that repeatedly blocks for a short sleep. Run this experiment on Linux; other Unix-like kernels may classify these counters differently.

Compile and run both modes:

The exact counts vary between runs and machines. The blocking mode should normally produce roughly one voluntary switch for each sleep. The CPU mode should produce few voluntary switches; its involuntary count depends on system load and scheduler competition.

The experiment demonstrates an important interpretation rule: many voluntary switches can simply mean that a program intentionally spends much of its time waiting rather than wasting CPU.

In a multithreaded Linux program, RUSAGE_SELF aggregates usage across the process's threads. The Linux-specific RUSAGE_THREAD option can measure only the calling thread.

When Context Switching Becomes a Performance Problem

A high switch rate is a clue, not a diagnosis.

For an I/O server, frequent voluntary switches may be entirely appropriate. For a CPU-bound service, a large number of involuntary switches can indicate that too many runnable threads are competing for the available CPUs.

Common sources of harmful switching include:

  • Creating far more CPU-bound workers than available CPUs
  • Dividing work into tasks so small that handoff overhead dominates
  • Heavy lock contention that repeatedly sleeps and wakes threads
  • Rapid handoffs between pipeline stages
  • Running unrelated CPU-intensive workloads on the same CPUs

The consequences can include lower throughput, colder caches, more scheduling delay, and worse tail latency.

The corrective action depends on the cause. Useful options include right-sizing CPU-bound worker pools, reducing lock contention, batching tiny units of work, and avoiding unnecessary thread creation. I/O-heavy workloads may reasonably use more concurrency than the CPU count because many workers are asleep, but the right amount must be measured under realistic load.

Trying to reduce all context switches is the wrong goal. Removing a blocking wait by busy-spinning, for example, can reduce voluntary switches while wasting an entire CPU. The goal is efficient progress and predictable latency.

Reading the Counters Carefully

Context-switch metrics need surrounding evidence.

Compare rates, not only lifetime totals

A process that has run for months can have a huge cumulative count without any current problem. Measure the change over a known interval or use a rate-reporting tool.

Separate voluntary from involuntary

A rise in voluntary switches points toward blocking, sleeping, or synchronization. A rise in involuntary switches points more strongly toward CPU competition or preemption.

These are directions for investigation, not proofs. The application and workload determine what is normal.

Inspect individual threads

One overloaded worker can disappear inside process-wide totals. Per-thread counts reveal whether switching is evenly distributed.

Correlate with CPU and latency

Combine switch counts with CPU utilization, runnable-queue pressure, migrations, and request-latency measurements. A switch rate that looks large in isolation may have no meaningful effect on throughput or latency.

Measure the intended workload

Debug logging, tracing, and profilers can introduce additional scheduling activity. Compare like-for-like runs and repeat measurements before drawing conclusions.

Summary

  • A context switch saves one thread's execution state and restores another's.
  • Blocking and yielding commonly cause voluntary switches; preemption commonly causes involuntary switches.
  • A user-to-kernel mode transition is not a context switch unless a different thread is selected.
  • Switching between processes can require an address-space change, while threads in one process normally share the same address space.
  • Direct kernel work and lost cache or TLB locality both contribute to cost.
  • Linux exposes per-task voluntary and nonvoluntary counters, but rates must be interpreted alongside CPU pressure and application latency.
  • Context switching enables efficient multitasking; optimization should target unnecessary contention and oversubscription, not switching itself.

Quiz

Context Switching Quiz

5 quizzes