Imagine a busy host taking an order, sending it to the kitchen, then helping another diner while the food cooks. Cooperative multitasking works similarly: a running task pauses voluntarily so another task can make progress. The task must choose when to yield; the cooperative scheduler cannot take control away from it. section 12.0 section 12.3
In preemptive multitasking, the operating system can pause a thread. In a cooperative system, tasks instead hand control to an event loop at chosen points. Coroutines make this practical: each can pause, retain its progress, and resume later, allowing many tasks to share one execution thread. section 12.3 section 12.4
Taking turns in Python
A small Python event loop keeps coroutines in a queue. It resumes the next one, then puts it back in the queue if it has yielded rather than finished. A Fibonacci coroutine prints one value per turn, making each handoff visible. section 12.4
# Chapter 12/coroutine.py
from collections import deque
import typing as T
Coroutine = T.Generator[None, None, int]
class EventLoop:
def __init__(self) -> None:
self.tasks: T.Deque[Coroutine] = deque() #1
def add_coroutine(self, task: Coroutine) -> None:
self.tasks.append(task) #2
def run_coroutine(self, task: Coroutine) -> None:
try:
task.send(None) #3
self.add_coroutine(task)
except StopIteration: #4
print("Task completed")
def run_forever(self) -> None: #5
while self.tasks: #5
print("Event loop cycle.") #5
self.run_coroutine(self.tasks.popleft()) #5
def fibonacci(n: int) -> Coroutine:
a, b = 0, 1
for i in range(n):
a, b = b, a + b
print(f"Fibonacci({i}): {a}")
yield #6
return a #7
if __name__ == "__main__":
event_loop = EventLoop()
event_loop.add_coroutine(fibonacci(5))
event_loop.run_forever()
The queue, send(None) call, and StopIteration handler show how the loop resumes, reschedules, and retires coroutines. section 12.4
The same pattern can handle a server: its coroutine yields while accepting a connection, and an event loop coordinates the resulting client tasks within one thread. This is especially useful when many tasks spend time waiting for I/O, such as network requests. section 12.9 section 12.4
The trade-off is control over the handoff:
- Lightweight user-level tasks can cost less to manage and switch than system threads. section 12.5
- Choosing yield points can keep another task from interrupting a critical section midway through. section 12.5
- A long-running task that never yields can hold up every other task. Break such work into smaller chunks or return control periodically; concurrency alone does not make that work faster. section 12.5
