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 forcall_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.__stepcallscoro.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_resultschedules the waiting Tasks withcall_soon. - Ordering consequences:
create_taskdoes not start the coroutine until the current task suspends;call_soonis 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.