Async, Await and Asyncio in Python

Hello, in this post I will discuss about the async and await keyword introduced in Python 3.5 along with asyncio module.

Let's first discuss about execution of code in sequential and concurrent mode.

Sequential Computing -

Sequential Computing means executing the current instruction before moving onto the next one.

Let's understand that through an example -

from time import sleep
from datetime import datetime

# storing the time at which the script start executing
start = datetime.now()

def func1():
    print("func1")
    sleep(5)
    print("Complete executing func1")
    
def func2():
    print("func2")
    sleep(5)
    print("Complete executing func2")
    
# calling both functions
func1()
func2()
print("Total Excecution Time: "+str((datetime.now())-start))

Output -

func1
# sleeping for 5 seconds
Complete executing func1
func2
# sleeping for 5 seconds
Complete executing func2
Total Excecution Time: 0:00:10.007661

In above code you can see, I created two functions and added sleep method with 5 seconds as sleeping time in each function. Then, I called func1 and func2 respectively. Since, func1 was called first. Thus, it will be executed at first and then func2 will be executed. This is sequential computing. It took total time of around 10 seconds to complete the execution.

This can be said as First Come First Serve.

Concurrent Computing -

Concurrent computing means executing several instructions concurrently or at the same time.

Let's understand it through an example -

import asyncio
from datetime import datetime

# storing the time at which the script start executing
start = datetime.now()

async def func1():
    print("func1")
    await asyncio.sleep(5)
    print("Complete executing func1")
    
async def func2():
    print("func2")
    await asyncio.sleep(5)
    print("Complete executing func2")
    
# calling both functions
async def main():
    await asyncio.gather(func1(), func2())
asyncio.run(main())
print("Total Excecution Time: "+str((datetime.now())-start))

Output -

func1
func2
# sleeping for 5 seconds
Complete executing func1
Complete executing func2
Total Excecution Time: 0:00:05.006706

In above code you can see, I created two functions and added asyncio.sleep() method with 5 seconds as sleeping time in each function. Then, I used asyncio.gather() method to run the coroutines (func1 and func2) concurrently. Then, I used asyncio.run() method to execute the coroutine (main). In contrast with Sequential computing code, this code just took around 5 seconds to complete the execution.

So, from the above the two examples, you should have got a basic idea about the difference between Sequential and Cuncurrent computing.

Now, let's dive more deeper into Concurrent computing.

Asyncio commonly used methods and keywords -

  • Concurrency- Executing several instructions at the same time.

  • Sequential - Executing single instruction at a time.

  • Coroutine - In simple terms, a coroutine is declared using async def keyword and is generalized form of subroutine. It can be entered, exited, and resumed at many different points. Meaning, a coroutine can be exited and then resumed again from that same point like we did in the above code using asyncio.sleep() method.

  • asyncio module - asyncio module is used to write concurrent code using async and await keywords.

  • async keyword - async keyword is used to declare the coroutine.

  • await keyword - await keyword passes the control back to the event loop. It tells the event loop to execute another instructions, coroutine, etc untill it's completion. Like, In the above example we used await keyword before asyncio.sleep() method to tell the event loop to execute other function func2 until it's completion. Thus, it printed func2 first before printing Complete executing func1.

  • asyncio.sleep() method - It is used to suspend the execution of a program for a specific amount of time as passed in the method. In contrast with time.sleep() method, it is non-blocking in nature. Meaning it let's the execution of other code until it completes its suspension time.

  • asyncio.run() method - It runs the passed coroutine and print the result returned by that coroutine.

  • asyncio.gather() method - It runs awaitable objects (objects which have await keyword) concurrently. Like in the above example, we used func1 and func2 within gather method. When all awaitables are completed successfully, the result is an aggregate list of returned values.

  • asyncio.wait() method - It is used to run awaitable objects in the set.

  • asyncio tasks - Tasks are used to schedule the execution of coroutines, since we can't 1000 of coroutines at the same time. Thus, we use tasks to schedule the coroutines.

  • asyncio.create_task() method - It is used to add a coroutine passed into this method to task and schedule its execution.

  • asyncio event loop - Event loop runs asynchronous tasks and callbacks.

  • asyncio.get_event_loop() method - It gets the current event loop.

  • asyncio.new_event_loop() method - It is used to create a new event loop object.

  • asyncio.run_until_complete() method - It is used to schedule to run as a asyncio tasks or runs the instance of asyncio.Future until its completed.

  • asyncio future - It is a special low-level awaitable object which represents an eventual result of an asynchronous operation.

Examples -

Example 1 -

import asyncio

async def func1():
    print("testing")

async def main():
    # scheduling func1 execution by adding it as a task
    task1 = asyncio.create_task(func1())
    
    # awaiting task1 until it's completed
    await task1
    
asyncio.run(main())

Output 1 -

testing

Example 2 -

import asyncio

# function to generate asynchronous interable range
async def a(num):
    for x in range(num):
        yield(x)

async def func1(q):
    print(q)
    await asyncio.sleep(2)
    print("finished "+str(q))

# list of tasks
tasks = []
async def main():
    async for x in a(10):
        # create_task method to schedule
        # the execution of function func1
        task = asyncio.create_task(func1(x))
        # adding the task to tasks list
        tasks.append(task)

# to get the current event loop
loop = asyncio.get_event_loop()
# run until function main complete its execution
loop.run_until_complete(main())
# run until all tasks complete their executions
loop.run_until_complete(asyncio.wait(tasks))

Output 2 -

0
1
2
3
4
5
6
7
8
9
# sleeping for 2 seconds
finished 0
finished 1
finished 2
finished 3
finished 4
finished 5
finished 6
finished 7
finished 8
finished 9

Asyncio isn't a easy topic to learn, it takes time but with time and practice you can get good in this. This was just a basic overview of async, await and asyncio module in Python. If you have any query regarding this, feel free to ask me in the discussion section below.

Thank you so much for reading this.