An introduction to asyncio in python
Hello asyncio world!

Introduction:
Asyncio is a tool for concurrent programming
in python
which is more light weight than threads and multi-processing
. This works based on a event-loop which executes a collection of tasks and the significance is , these tasks have the liberty to choose when to yield
control back to the event loop.
Target Users of asyncio:
The asyncio
library is targeted for both
- End-user developers
- Framework developers
We fall into the first category, where we build applications that will be used by some user. For instance, the application could be fastapi
application.
Framework developers are the ones who write libraries in python that will be consumed by end-user application developers like us.
Hello asyncio World!
The steps to write an program is as follows
- Create an event loop.
- Call async/await functions which are coroutines.
- Create a task to run on the loop.
- Wait for the tasks to complete.
- Close the loop.
Now, let’s get started with our first asyncio program.
import asyncio, time
async def hello_world():
print(time.ctime())
await asyncio.sleep(1.0)
print(time.ctime())
asyncio.run(hello_world())
Output:
Tue Feb 8 09:31:51 2022
Tue Feb 8 09:31:52 2022
Now, we have introduced two new keywords async
and await
. Any python function declared with async def
is called a coroutine
function.
The await
keyword always takes a parameter which is an awaitable.
The awaitable could either be a
- Coroutine (A function executed as a result of async def).
- Any object that implements the special method
__await__
.
Well, we discussed few steps to write an asyncio
program. However, the hello world program does not quite match them. Let's peel the layers one by one. What exactly happens under the hood of asyncio.run()
.
Under the hood of asyncio.run():
To understand what exactly asyncio.run()
does, let's rewrite the hello world asyncio program without using run()
method.
import asyncio, time
async def hello_world():
print(time.ctime())
await asyncio.sleep(1.0)
print(time.ctime())
loop = asyncio.get_event_loop()
task = loop.create_task(hello_world())
loop.run_until_complete(task)
loop.close()
Output:
Tue Feb 8 09:31:51 2022
Tue Feb 8 09:31:52 2022
This program does exactly the same as the first one except that this is little more comprehensive. We don’t use the run() method to execute the coroutine. Instead we create an event loop
and run tasks as part of the event loop
.
loop = asyncio.get_event_loop()
This step gets an event loop
. Why do we need one? The answer is simple. Coroutines
cannot be executed by calling them directly like regular python functions. They need an event loop to run. This is how we get one ☝️ . Calling get_event_loop()
anywhere outside a coroutine always gives the same instance.
task = loop.create_task(hello_world())
To execute a coroutine, creating a task
is mandatory. Without this , the coroutine
will never be executed. The create_task
schedules the coroutine to be run on an event loop. This will return a task object. The returned value can be extracted using task.result()
where task is the task object.
loop.run_until_complete(task)
This is a blocking call and will run until the coroutine is completed. asyncio.run()
internally calls this method and blocks the main thread similarly.
loop.close()
This is the final step and close() should be called on a stopped loop. A closed loop cannot be reinstated. asyncio.run() creates an event loop for every run and takes care of the shut down as well.
Summary:
I hope this article would have helped you to take your baby steps into the python asyncio world.
This is just an introduction to asyncio programming. I shall discuss more in my future articles about event loop, using context managers with asyncio etc. Any asyncio program should follow the above steps and it would be easy to understand asyncio library better.
Having said that, asyncio.run() is what we will be using mostly in our day to day programming. The steps were to understand what happens internally.
Caveats:
asyncio
does not make your program any faster. Get over it! It just avoids the context-switching that threads have to do and race-conditions.asyncio
does not remove GIL. It just doesn't have to deal with it as it is single threaded.asyncio
completely eliminates all race-conditions. Well! not really. It could avoid the race conditions that multithreaded programs may introduce such as shared memory access inside the same process. The main advantage is we exactly know where there are control transfers due to theawait
keyword which makes debugging easier.
Originally published at https://dock2learn.com on February 8, 2022.