Keeping track of asyncio.run execution.
Have you ever been curious about the inner workings of the asyncio.run()
function? If the answer is no, then get ready for a fascinating journey. This blog post will guide you through the process that takes place behind the scenes.
Before we begin.¶
We will be using as reference the CPython 3.11.4
implementation. Please note that in earlier versions of CPython, the event loop might have been implemented differently. This is an important note because we will be looking at the CPython implementation to see step-by-step the execution proccess followed when asyncio.run()
function is called.
asyncio.run(coro, *, debug=None)
¶
asyncio.run()
is used to run a coroutine in the event loop, it also creates a new event loop and closes it at the end.
Allright, what's next ? well, things are getting insteresting here. Actually, asyncio.run()
is shortcut for:
This means if we want to know what is happening when asyncio.run()
is called we shoud go to the Runner
class. Let's go there: 🙀
Note
I've deleted some code, comments and documentation from the below class, just to make it shorter.
Yes, I know that is long class, but we're going to focus on self.run()
& self._lazy_init()
methods. As we saw previously, when asyncio.run
is exuceted actually we're creating an instance of Runner
class and then we call its run
instance method.
When the Runner
class instance is created some instance attributes are created as well, one of most important is self._loop
which is set to None
. Now when the instance is created we can call its run
instance method, for that, we need to pass it a required argument coro
which is coroutine that we want to run in the event loop. As we see in the run
implementation (line 9) it starts making some validations, like:
- check if
coro
is actually a coroutine. - check if there is already an event loop running.
if the previous checks pass succesfully, so the self._lazy_init()
is called. This method also makes some checks but the most important here is the line 55:
The above line creates the event loop and assigs it to the instance variable self._loop
. Tracking the flow throught the code and assumming that self._lazy_init()
ran successfully (line 17), we can skip some irrelevant lines of code and go to the line 21:
it uses the previous created event loop and calls its create_task
instance method for wrapping the coro
passed to the run
method into task.
Digging a bit into self._loop.create_task
¶
To know a bit about what's happening when self._loop.create_task
is called, we need to go to BaseEventLoop
class which we can find in the file base_events.py. Let's bring the class from there and take a look at it.
Note
I've deleted some code, comments and documentation from the below class, just to make it shorter.
coro
pass to the self._loop.created_task
method an instance of Task
is created with the same coroutine coro
then, it's returned it at the end of BaseEventLoop's create_task
method. With this in mind, we can return to the run
execution.
Run coro
until complete.¶
After coro
is wrapped into a Task
(line 21), run
makes some other types of validations and finally the task is passed to run_until_complete
event loop instance method.
The run_until_complete
methods run the task passed as parameter until it's finished.
Sum up!¶
As you could see, there is nothing special underhood when the asyncio.run()
function is run, it only created and instance of Runner
class and then its run
instance method is executed, when this happens, there are some other methods executed, like create_task
and finally run_until_complete
.