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
corois 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.