Skip to the solution
HardDSA12 min read

How does the asyncio event loop schedule coroutines?

465 views

The problem

Question presented to candidate: "Inside asyncio there is one thread and possibly thousands of coroutines. How does the event loop decide what runs next? Walk me through what happens between create_task(), an await on a socket or a sleep, and the coroutine being resumed."

What a strong answer should cover:

  • The loop holds a ready queue of callbacks (a FIFO collections.deque), a heap of timers for call_later/call_at, and a selector (epoll, kqueue, ...) for I/O. Each iteration (_run_once) waits on the selector with a timeout based on the earliest timer, moves due timers and I/O callbacks to the ready queue, then runs exactly the callbacks that were ready at that moment.
  • The loop only runs callbacks; a Task turns a coroutine into callbacks: each Task.__step calls coro.send(None) until the coroutine yields a Future, then registers a done-callback that schedules the next step.
  • Futures connect the two worlds: when I/O completes or a timer fires, set_result schedules the waiting Tasks with call_soon.
  • Ordering consequences: create_task does not start the coroutine until the current task suspends; call_soon is FIFO; await asyncio.sleep(0) yields one turn; a coroutine that never awaits starves everything (cooperative scheduling).
  • 3.12 added asyncio.eager_task_factory, which runs a new task synchronously up to its first suspension; debug mode reports slow callbacks.

Code / implementation expected: Optional β€” a tiny scheduler model, or a script that prints the order in which tasks and callbacks run.

  • asyncio
  • event-loop

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

12 min read Β· 13 sections

Run the code

Edit the code and run it here. Each console line with an expected value becomes a test.

PythonA model scheduler: ready queue, timer heap, Futures and Tasks
PythonReal asyncio ordering: call_soon, create_task, eager tasks, fairness
# The real asyncio loop: what runs in which order. Runs on CPython 3.12+
# (asyncio.eager_task_factory is new in 3.12; asyncio.run needs no page loop).
import asyncio

order = []


async def child(name):
    order.append(f"{name} step 1")
    await asyncio.sleep(0)                 # yield: go to the back of the ready queue
    order.append(f"{name} step 2")


async def lazy_tasks():
    loop = asyncio.get_running_loop()
    loop.call_soon(order.append, "call_soon #1")
    loop.call_later(0, order.append, "call_later(0)")
    t1 = asyncio.create_task(child("t1"))  # queued; does NOT run yet
    t2 = asyncio.create_task(child("t2"))
    loop.call_soon(order.append, "call_soon #2")
    order.append("main still running: create_task did not switch")
    await asyncio.sleep(0)                 # main yields for the first time
    order.append("main resumed")
    await asyncio.gather(t1, t2)
    order.append("main done")


async def eager_tasks():
    asyncio.get_running_loop().set_task_factory(asyncio.eager_task_factory)
    t1 = asyncio.create_task(child("e1"))  # runs NOW, up to its first await
    order.append("main after creating e1")
    await t1
    async def instant():
        return "x"                         # never awaits anything
    quick = asyncio.create_task(instant())
    slow = asyncio.create_task(asyncio.sleep(0))
    order.append(f"instant done before any yield? {quick.done()}")
    order.append(f"sleep(0) done before any yield? {slow.done()}")
    await slow


async def ticker():
    for i in range(3):
        order.append(f"  tick {i}")
        await asyncio.sleep(0)


async def greedy():
    for i in range(3):
        order.append(f"greedy {i}")        # no await inside: never yields


async def cooperative():
    for i in range(3):
        order.append(f"coop {i}")
        await asyncio.sleep(0)             # a checkpoint: let others run


async def fairness():
    await asyncio.gather(ticker(), greedy())
    order.append("----")
    await asyncio.gather(ticker(), cooperative())


for demo in (lazy_tasks, eager_tasks, fairness):
    order.clear()
    asyncio.run(demo())
    print(f"--- {demo.__name__}")
    for i, line in enumerate(order, 1):
        print(f"{i:2}. {line}")

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 131 of 139 in Python

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Python questions
Up nextHard

What is the difference between __getattr__ and __getattribute__?

Continue