Python AsyncIO Event Loop
Introduction
Python asyncio
is a library for efficient single-thread concurrent applications. Ever since I started to use it, unlike Python multiprocessing
and threading
, it has been like a mysterious black box to me. Although I could still use asyncio
for some simple high-level concurrent applications by taking advantage of the open source asyncio
libraries, such as asyncssh
and httpx
, I have no idea how those asyncio
libraries were implemented from scratch. To understand the asyncio
mechanism, it might be necessary to look at its low level implementation details.
Event loop is the core of Python asyncio
. Every coroutine
, Future
, or Task
would be scheduled as callback
and be executed by an event loop. In this blog post, I would like to look into Python event loop at the low-level implementation superficially.
Event Loop
Although asyncio
widely uses coroutine
, Future
, or Task
, it is not necessary to use them in order to run tasks on an event loop. Event loop ultimately runs scheduled callback
s. To see this, let’s check the implementation of loop.run_forever
from Python 3.8.
1 | def run_forever(self): |
Without going into the details, our gut feeling tells us that the key function call in the run_forever
is the self._run_once()
. The self._run_once
function is described as follows.
1 | def _run_once(self): |
This information is somewhat reflected in the loop.run_forever documentation. Event loop must have loops and run iteration by iteration, otherwise its name would not have been event loop. But what exactly is an iteration of the event loop? Before checking the actual implementation, I imagined an iteration of even loop is a fixed finite length of time frame where callbacks
could be executed. We could use a for
/while
loop in the iteration and the end of iteration could be determined by measuring the UNIX time at the end of each for
/while
execution. But this raises a problem. What if there is a callback
that takes very long time to run in the for
/while
loop and keeps blocking the thread, then the fixed length of the time frame could not be guaranteed. It turns out that the design of an actual event loop iteration in Python is somewhat similar but more delicate.
All the scheduled callbacks
for the current event loop iteration are placed in self._ready
. By looking at the implementation superficially, it seems that we have a (heap/priority) queue of scheduled callbacks
, some of which might have been delayed and canceled. Although the loop.run_forever
runs forever, it does have timeout for each event loop iteration. For the “call later” callbacks
that are scheduled to run after the current UNIX time, they are not ready so they will not be put into the self._ready
.
1 | sched_count = len(self._scheduled) |
Only the callbacks
in the self._ready
will be executed in order.
1 | # This is the only place where callbacks are actually *called*. |
This means that in an event loop iteration, the number of callbacks
being executed is dynamically determined. It does not have fixed time frame, it does not have an fixed number of callbacks
to run. Everything is dynamically scheduled and thus is very flexible.
Notice that this self._run_once
is only called in the loop.run_forever
method, but not others. Let’s further check the more commonly used method loop.run_until_complete which is being called by asyncio.run
under the hood.
1 | def run_until_complete(self, future): |
The most prominent function call is self.run_forever()
surprisingly. But where are the Future
scheduled as callbacks
in the event loop. tasks.ensure_future
which takes both Future
and loop
as inputs scheduled the callback
s. In the tasks.ensure_future
, it calls loop.create_task(coro_or_future)
to set the callback
schedules in the event loop. Also note that there is additional callback
_run_until_complete_cb
added to the event loop so that the self.run_forever()
will not actually run forever.
1 | def ensure_future(coro_or_future, *, loop=None): |
The loop.create_task
is a public interface and the documentation could be found from the Python website.
1 | def create_task(self, coro, *, name=None): |
Conclusions
Although we did not go through all the code about the event loop, we have become more knowledgeable about how a Python event loop executes call backs.
Python AsyncIO Event Loop