skip to content

questions

5

Explain the difference between preemptive and cooperative scheduling of concurrent tasks, and what each model demands from the code being scheduled.

level: juniorimportance: must knowfreq 68%

answer

  1. who decides the switch point?
  2. preemptive = timer interrupt, liveness guaranteed
  3. cooperative = yield only, cheap + fewer interleavings
  4. one non-yielding task stalls all peers
  5. real systems layer both; safepoints are the middle ground

basics

~20 s

Under preemptive scheduling a timer interrupt lets the scheduler suspend a task at almost any instruction and run another. Under cooperative scheduling a task runs until it voluntarily yields. Preemption guarantees progress for everyone; cooperation is cheaper and predictable but one non-yielding task blocks all others.

solid answer

~50 s

**Preemptive** scheduling means the scheduler can take the CPU away from a running task without its consent, usually driven by a timer interrupt at the end of a time slice or when a higher-priority task becomes runnable. Its guarantee is liveness: a compute-bound task cannot monopolize a core, and a misbehaving task cannot freeze the system. **Cooperative** scheduling means a switch only happens where the task lets it — at an explicit yield or a suspension point such as awaiting an operation. Because switches occur only at known points, they are cheap, state to save is minimal, and a task can assume nothing else runs between two of its own yield points, which removes many interleavings. The price is a hard contract: any long computation or blocking call without a yield stalls every other task sharing that scheduler. That is exactly the "one slow handler freezes the event loop" failure. In practice modern systems layer them: preemptive kernel threads at the bottom, cooperative user-level tasks multiplexed on top.

code

text · 6 lines
text
Cooperative (one thread), task B never yields:
  A: run..yield | B: run.....................(no yield)........ | C never runs

Preemptive (one core), quantum = q:
  A: run q | B: run q | C: run q | A: run q | B: run q | ...
  (B is interrupted mid-computation; everyone progresses)

go deeper

for a junior

State who decides the switch: the scheduler via a timer versus the task via a yield, and give the classic failure of a task that never yields.

for a middle

Add mechanism and consequence: interrupt-driven full context save versus a cheap suspension-point switch, and why preemption forces synchronization on shared state.

for a senior

Talk about the layered reality — cooperative tasks over a preemptive thread pool — and diagnose the blocking-call-starves-the-loop incident with concrete remedies.

for a principal

Treat it as a spectrum defined by who chooses switch points and at what granularity, including compiler-inserted safepoints, and the latency-versus-overhead tradeoffs that choice implies.

## The core question a scheduler answers Many runnable tasks, fewer CPUs. Something must decide who runs and, crucially, **when a running task stops running**. The two families differ only on that second point. ## Preemptive scheduling A periodic timer interrupt hands control to the scheduler regardless of what the task was doing. The scheduler saves the interrupted task's registers and program counter, picks another runnable task, restores its state and resumes it. Preemption also happens on other events — a higher-priority task waking, a blocking operation, an interrupt. What it guarantees: - **Liveness by construction.** An infinite loop with no system calls still cannot hold a core forever. - **Bounded latency for high-priority work.** A newly-runnable urgent task can displace a running one rather than queueing behind it. - **No contract with application code.** Ordinary programs need no awareness of the scheduler. What it costs: - **A switch can occur between any two instructions**, so any multi-step update to shared state can be observed half-done. This is precisely why shared mutable state needs synchronization in preemptively-scheduled systems. - **Full context must be saved**, and switches happen at points the code did not choose, which is worse for caches. - **Nondeterminism.** Interleavings vary run to run, which is why concurrency bugs reproduce badly. ## Cooperative scheduling The running task keeps the CPU until it voluntarily gives it up: an explicit yield, or an operation that suspends it (awaiting I/O, waiting on a channel). The scheduler is invoked from within the task's own execution, not from an interrupt. What it buys: - **Cheap switches.** Only the state needed to resume at a known suspension point is saved — often far less than a full register and kernel-stack save — and the switch may not enter the kernel at all. - **Fewer interleavings to reason about.** Between two of its own yield points, a task can treat itself as atomic with respect to its scheduler peers on that thread. Some invariants that would need a lock become free. (This holds only within one cooperative scheduler on one thread; the moment those tasks are spread across multiple preemptive threads, ordinary shared-state hazards return.) - **Predictable switch points**, which makes tracing and profiling easier. What it demands: - **Every task must yield often enough.** A tight computation, a synchronous file read, or an accidental blocking call inside a cooperative task stalls all its peers. There is no interrupt to rescue them. - **Fairness is the application's responsibility.** A task that yields rarely starves the rest; the scheduler cannot enforce a share. - **Priority is weak.** An urgent task waits for the current one to reach a suspension point, so latency is bounded by the longest gap between yields, not by a quantum. ## Why systems use both Almost every real platform layers the models. The operating system schedules kernel threads preemptively, so no process can hang the machine. On top of that, runtimes multiplex thousands of lightweight tasks cooperatively over a small pool of those threads, because a cooperative switch is far cheaper than a kernel thread switch and lets you keep enormous numbers of concurrent, mostly-waiting operations. That layering explains a common production incident. A blocking or CPU-heavy call inside a cooperative task does not just delay that task; it occupies the underlying thread, and every other task assigned to that thread stops progressing. The runtime cannot preempt it. The fixes are the standard ones: move blocking work to a dedicated pool sized for it, break long computations with explicit yields, or use a runtime that can preempt at safepoints. ## Middle grounds Some runtimes insert **implicit yield checks** at function calls, loop back-edges or allocation points, so long-running tasks are preempted at *safe* points chosen by the compiler rather than at arbitrary instructions. That recovers much of preemption's liveness while keeping switch state small and the switch point well-defined. Recognizing that this middle ground exists — that "preemptive versus cooperative" is a spectrum defined by *who chooses the switch point and how finely* — is what distinguishes a strong answer from a memorized contrast.

  • In a cooperative system, what exactly goes wrong when a task performs a long blocking call?
    The task never reaches a suspension point, so the scheduler is never entered and no peer on that thread makes progress — latency for all of them grows by the length of the call. It is not just slower; unrelated work is stalled. Remedies are moving blocking work to a separate pool sized for blocking, or breaking the work up with explicit yields.
  • Cooperative scheduling is said to remove some interleavings. When does that reasoning become unsafe?
    Only while the cooperating tasks share a single underlying thread. As soon as the runtime spreads them over a pool of preemptively-scheduled threads, two tasks can genuinely run at the same instant on different cores, and any shared mutable state needs real synchronization again. Treating 'my task is atomic between yields' as a global guarantee is a common and expensive mistake.

Preemptive is a debate with a moderator who cuts your microphone at time; cooperative is a conversation where you speak until you pause for breath. The second is smoother until someone never pauses.

saying these in an interview costs you the question

  • Saying cooperative scheduling is simply obsolete, ignoring that event loops and lightweight-task runtimes rely on it.
  • Believing a preemptive scheduler switches only at method or system-call boundaries.
  • Assuming yields happen automatically in a cooperative model without the code providing suspension points.
  • Claiming 'no locks needed' for cooperative tasks even when they run across a multi-thread pool.
  • Confusing preemption with parallelism — preemption is about interruption, not about multiple cores.

context

open as a page

What actually happens when the operating system switches from running one thread to another, and where does the cost of that switch come from?

level: middleimportance: must knowfreq 58%

basics

~20 s

The kernel saves the running thread's registers and program counter, picks another runnable thread, and restores its state; a switch to a different process also swaps page tables. The direct cost is small. The larger, indirect cost is cold caches, branch predictors and address-translation entries when the new thread starts running.

open as a page

Why do preemptive schedulers give tasks a bounded time slice, and how do priorities interact with fairness when many tasks are runnable at once?

level: middleimportance: should knowfreq 42%

basics

~20 s

A time slice bounds how long one task can hold a core, trading throughput for responsiveness: long slices amortize switch cost, short slices cut waiting time for others. Priorities bias the choice of who runs next; strict priority can starve low-priority work, so schedulers add aging or proportional shares.

open as a page

What happens to throughput and latency when a machine has far more runnable threads than CPU cores, and how would you recognize that state from the outside?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Beyond one runnable thread per core, throughput stops rising and eventually falls: extra threads add context switches, cache eviction and lock contention rather than work. Latency grows roughly with the queue of runnable threads. From outside you see high run-queue length, many involuntary context switches, high CPU with falling completion rate, and rising tail latency.

open as a page

What are CPU affinity and per-core run queues, and when is deliberately pinning work to specific cores worth the loss of scheduler flexibility?

level: principalimportance: nice to knowfreq 28%

basics

~20 s

Schedulers keep per-core run queues and prefer to resume a thread on its previous core, because its cached data is there; idle cores steal work from busy queues. Explicit pinning fixes a thread to chosen cores, protecting cache and memory locality for latency-critical work, at the cost of the scheduler's ability to balance load.

open as a page