Async Models
20 min
Threads achieve concurrency via true (or OS-scheduled) parallelism, with
all the race-condition risk from the previous modules. Async/event
loops take a completely different approach: a SINGLE thread runs one
task at a time, but each task voluntarily yields control back to the
loop at defined points (an await in real asyncio) instead of running
to completion. The loop then picks the next ready task. Because only one
task ever actually executes at once, there's no read-modify-write race
condition to worry about — the tradeoff is that a task which never
yields blocks everything else.
def run_event_loop(tasks):
queue = list(tasks.keys())
remaining = {tid: list(steps) for tid, steps in tasks.items()}
trace = []
while queue:
tid = queue.pop(0)
if remaining[tid]:
step = remaining[tid].pop(0)
trace.append((tid, step))
if remaining[tid]:
queue.append(tid) # not done yet -- go to the back of the line
return trace
This queue.pop(0) / queue.append(tid) pattern is the exact same round-robin rotation used in Networking's load-balancing module -- here it's rotating which TASK gets the CPU next, instead of which SERVER gets the next request.
Write `run_event_loop(tasks)`: `tasks` is a dict mapping task ID to a list of step labels. Simulate a single-threaded, cooperative round-robin scheduler: repeatedly take the task at the front of the queue, run ONE of its remaining steps (append `(task_id, step)` to the trace), and if that task still has steps left, put it back at the END of the queue. Return the full trace.
How does Python's asyncio event loop achieve concurrency WITHOUT using multiple OS threads?
You can simulate cooperative round-robin task scheduling, and understand why a single-threaded event loop sidesteps the race conditions that plague preemptively-scheduled threads.