Explain the difference between preemptive and cooperative scheduling of concurrent tasks, and what each model demands from the code being scheduled.
answer
- who decides the switch point?
- preemptive = timer interrupt, liveness guaranteed
- cooperative = yield only, cheap + fewer interleavings
- one non-yielding task stalls all peers
- real systems layer both; safepoints are the middle ground
basics
~20 sUnder 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 linesCooperative (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
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.
Add mechanism and consequence: interrupt-driven full context save versus a cheap suspension-point switch, and why preemption forces synchronization on shared state.
Talk about the layered reality — cooperative tasks over a preemptive thread pool — and diagnose the blocking-call-starves-the-loop incident with concrete remedies.
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.