Updated On : Sep-11,2022 Time Investment : ~30 mins

asyncio - Concurrent Programming using Async / Await Syntax in Python

> What is Async / Await Programming?

Async/Await is a programming pattern that allows us to asynchronously execute a function without blocking.

The normal function would execute line by line and will only proceed to execute the next line after it has completed running all previous statements in sequence.

This might not always be necessary. There are functions in which parts of the function are not dependent on each other and can be run out of sequence to generate results.

The CPU can be assigned to other statements of the function until a particular statement has not returned and is waiting for IO, network input, etc.

Async/Await programming lets us run a function in parallel in a single thread of the computer where tasks of the functions explicitly give up CPUs when they don't need it.

> How Async/Await is Different From MultiThreading / Multiprocessing?

Async/Await programming is generally referred to as cooperative multitasking or non-preemptive multitasking whereas multithreading is generally referred to as preemptive multitasking.

The reason behind this is that in the case of asynchronous programming using async/await, the tasks of the code give up CPU by themselves voluntarily when they are not using it, without the scheduler explicitly forcing/interrupting them to give up CPU.

In the case of multithreading, the scheduler interrupts threads to give up CPU when they are not using it.

The scheduler in the case of asynchronous async/await programming just starts the tasks and allocates other tasks to the CPU when a particular task voluntarily gives up the CPU. The role of the scheduler is more in the case of multithreading / multiprocessing compared to asynchronous async/await programming.

> What Solutions Python Offers for Concurrent Programming using Async / Await Syntax?

Python provides support for Async/Await programming using 'async', 'await' keywords, and 'asyncio' module.

> How Async / Await Keywords Work in Python?

Python lets us declare functions with 'async' keyword hinting to the Python interpreter that it is suitable for an asynchronous run. The functions declared with 'async' keywords are generally referred to as coroutines in Python because they yield CPU when not using it.

Once a function is declared asynchronous using 'async' keyword, we can use await keyword in a statement calling that function. This will hint Python interpreter that the statement (function call) awaited with await will return eventually and the interpreter can in meantime continue execution with the next statements of the function as they might not be dependent on that awaited statement result.

Only functions declared with async keyword can be awaited using await keyword in Python.

> What Can You Learn From This Article?

As a part of this tutorial, we have explained how to write concurrent code in Python using "async" & "await" keywords with simple and easy-to-understand examples. Tutorial then goes on to explain the usage of Python module 'asyncio' that provides helpful functions for asynchronous programming. Tutorial covers topics like how to create tasks to execute coroutines in parallel using "asyncio", execute multiple coroutines in parallel, collect coroutines results as they complete, make coroutines wait for other coroutines, cancel coroutines, etc.

Below, we have listed important sections of Tutorial to give an overview of the material covered.

Important Sections Of Tutorial

  1. Simple Example Demonstrating Use Of "Async / Await" Keywords
  2. Create Tasks for Parallel Execution using "create_task()" Method
  3. Execute Multiple Coroutines using "gather()" Method
    • 3.1: Using "gather()" with Coroutines which does not Returns Result
    • 3.2: Using "gather()" with Coroutines which Returns Results]
  4. Retrieve Coroutines Results as They Complete using "as_completed()" Method
  5. Make Coroutine Wait for Other Coroutines using "wait()" Method
  6. Make Coroutine Wait for Other Coroutines using "wait_for()" Method
  7. Retrieve Current Task and All Tasks using "current_task()" and "all_task()" Methods
  8. Cancel Tasks (Coroutines) using "cancel()" Method

NOTE

Please make a NOTE that all the examples in this tutorial are executed using Python version 3.9.1. Some of the examples will require minor changes if ran on previous versions due to the unavailability of few methods in previous versions.

1. Simple Example Demonstrating Use Of "Async / Await" Keywords

As a part of our first example, we'll simply explain how we can create a code with async/await syntax and run it.

Our code for this part creates few functions whose use we have explained below.

  • addition() - It adds two numbers and then prints result. It takes 3 seconds for this task.
  • multiplication() - It multiplies two numbers and then prints the result. It takes 1 second for this task.
  • division() - It divides two numbers and then prints result. It takes 5 seconds for this task.
  • subtraction() - It subtracts two numbers and then prints result. It takes 7 seconds for this task.

All of the functions are declared with async keywords to mark them as coroutines.

All four arithmetic operations functions have await which calls a sleep() function of asyncio module that waits for a specified number of seconds. The reason behind using this call is that it'll give up CPU when not used for that many seconds.

All our examples used Python "time" Module to record time taken.


> Important Methods of 'asyncio' Module

  • run(coroutine, debug=False) - This method takes as input a coroutine and runs it. This method can be the starting point of asynchronous code execution. It'll take care of all coroutines calls inside of called coroutine.
  • sleep(delay, result=None) - This function accepts numbers as input and blocks the caller for that many seconds. If result parameter is provided with some value then that value will be returned to the caller after this function completes.

We can notice from the output that each function has run in sequence and not in parallel. It takes nearly 16 seconds to complete the script.

The reason that code does not execute in parallel because we have not created tasks out of functions that are eligible to run in parallel.

NOTE

Please make a NOTE that async / await keywords are hint that specifies functions that can be executed asynchronously. It won't run in parallel. When we create tasks in next example, then only asyncio will run asynchronously.

Declaring and calling function with async and await keywords are only hint that they can be executed asynchronously.

We'll explain how to create tasks to run this code in parallel in next example.

import asyncio
from datetime import datetime
import time

async def addition(a,b):
    await asyncio.sleep(3)
    print("Addition Result       : ", a + b)

async def multiplication(a,b):
    await asyncio.sleep(1)
    print("Multiplication Result : ", a * b)

async def division(a,b):
    await asyncio.sleep(5)
    print("Division Result       : ", a / b)

async def subtraction(a,b):
    await asyncio.sleep(7)
    print("Subtraction Result    : ", a - b)


async def main():

    await division(10,20)

    await subtraction(10,20)

    await addition(10,20)

    await multiplication(10,20)


print("Start Time : ", datetime.now(), "\n")
start = time.time()

asyncio.run(main())

print("\nEnd   Time : ", datetime.now())
print("\nTotal Time Taken : {} Seconds".format(time.time() - start))

OUTPUT

Start Time :  2021-02-21 10:07:53.855844

Division Result       :  0.5
Subtraction Result    :  -10
Addition Result       :  30
Multiplication Result :  200

End   Time :  2021-02-21 10:08:09.875059

Total Time Taken : 16.019198417663574 Seconds

2. Create Tasks for Parallel Execution using "create_task()" Method

As a part of our second example, we'll explain how we can create a task around coroutines and execute them in parallel using asyncio module method create_task().

Our code for this part has declared arithmetic functions exactly like the previous example. Our main function has wrapped call to each arithmetic coroutine inside of asyncio.create_task() method. The method will return Task object. We have then awaited each task instance inside of the main function waiting for them to complete.


> Important Methods of 'asyncio' Module

  • create_task(coroutine, name=None) - This method takes as input coroutine and returns Task instance eligible to execute in parallel. It also accepts name parameters which can be used to assign a name to a task.

> Important Methods of Task Instance

  • get_name() - This method returns name of the task.
  • set_name(name) - This method sets the name of the task.
  • get_coro() - This method returns underlying coroutine of the task.
  • result() - This method returns results of task.

We can notice from the output of this example that the sequence of output is changed as well as it took only 7 seconds to complete the main coroutine. The reason behind this is that coroutine that was not using CPUs gave it up for other tasks.

Due to this, multiplication coroutine which takes a time of 1 second completed first, followed by addition which takes 3 seconds, followed by division which takes 5 seconds, and finally subtraction which takes 7 seconds.

As all coroutines/tasks got executed in parallel, it took only 7 seconds which is a time of subtraction coroutine.

NOTE

Please make a NOTE that once we create a task using create_task() method, it starts executing coroutines and returns Task instance immediately. We can use this task instance for various purposes like cancel tasks, wait for the task to complete, etc.

import asyncio
from datetime import datetime
import time

async def addition(a,b):
    await asyncio.sleep(3)
    print("Addition Result       : ", a + b)

async def multiplication(a,b):
    await asyncio.sleep(1)
    print("Multiplication Result : ", a * b)

async def division(a,b):
    await asyncio.sleep(5)
    print("Division Result       : ", a / b)

async def subtraction(a,b):
    await asyncio.sleep(7)
    print("Subtraction Result    : ", a - b)


async def main():

    div_task = asyncio.create_task(division(10,20))

    subtract_task = asyncio.create_task(subtraction(10,20))

    add_task = asyncio.create_task(addition(10,20))

    mul_task = asyncio.create_task(multiplication(10,20))

    await div_task
    await subtract_task
    await add_task
    await mul_task

print("Start Time : ", datetime.now(), "\n")
start = time.time()

asyncio.run(main())

print("\nEnd   Time : ", datetime.now())
print("\nTotal Time Taken : {} Seconds".format(time.time() - start))

OUTPUT

Start Time :  2021-02-21 10:13:10.696559

Multiplication Result :  200
Addition Result       :  30
Division Result       :  0.5
Subtraction Result    :  -10

End   Time :  2021-02-21 10:13:17.699999

Total Time Taken : 7.00343656539917 Seconds

3. Execute Multiple Coroutines using "gather()" Method

As a part of our third example, we'll explain how we can call many awaitable instances using a single method then calling await keyword many times. We'll explain the usage of gather() method which runs awaitables as tasks in parallel.


> Important Methods of 'asyncio' Module

  • gather(awaitables, return_exceptions=False) - This function accepts a list of awaitables (awaited coroutines) as input and returns their results once all awaitables have completed running. It returns results in the same sequence in which awaitables were given as input. If input awaitable is not scheduled as a task then it'll create a task output it and run it. If return_exceptions is True then an exception occurring in the task will be returned as result. If any awaitables fail with an exception, it won't impact others.

3.1 Using "gather()" with Coroutines which does not Returns Result

Our code for this part builds from the last example. We have again used the same four arithmetic coroutine definitions. Our code inside the main coroutine is modified.

We have wrapped all call to arithmetic coroutines inside of gather() method and have awaited gather() method so that the main waits for its completion. The remaining code is the same as in previous examples.

We can notice from that output that all coroutines have executed in parallel even though we had not created tasks out of them. The reason behind this is that gather() method creates a task for each coroutine passed to it. It takes nearly 7 seconds to complete this code running like the previous example.

The results are printed in a different sequence than the coroutines given as input to gather() method. The reason behind this is that we were not able to capture the results of the coroutines.

We'll solve this in part of this example which will print results in the same sequence in which coroutines were given as input to gather() method.

import asyncio
from datetime import datetime
import time

async def addition(a,b):
    await asyncio.sleep(3)
    print("Addition Result       : ", a + b)

async def multiplication(a,b):
    await asyncio.sleep(1)
    print("Multiplication Result : ", a * b)

async def division(a,b):
    await asyncio.sleep(5)
    print("Division Result       : ", a / b)

async def subtraction(a,b):
    await asyncio.sleep(7)
    print("Subtraction Result    : ", a - b)


async def main():

    await asyncio.gather(division(10,20),
                         subtraction(10,20),
                         addition(10,20),
                         multiplication(10,20))


print("Start Time : ", datetime.now(), "\n")
start = time.time()

asyncio.run(main())

print("\nEnd   Time : ", datetime.now())
print("\nTotal Time Taken : {} Seconds".format(time.time() - start))

OUTPUT

Start Time :  2021-02-21 10:26:49.446259

Multiplication Result :  200
Addition Result       :  30
Division Result       :  0.5
Subtraction Result    :  -10

End   Time :  2021-02-21 10:26:56.449867

Total Time Taken : 7.003642320632935 Seconds

3.2 Using "gather()" with Coroutines which Returns Results

Our code for this part has modified the definition of four arithmetic coroutines. Instead of printing results, they now return two tuples where the first value of the tuple is the name of the operation and the second value is the actual result.

The main coroutine now captures the result returned by gather() method inside a variable and then prints it. The rest of the code is exactly the same as in previous examples.

When we run the script, we can notice from the output that it also takes 7 seconds to complete like previous examples and returns the same results as previous examples. The only difference in the output is the sequence in which operations are printed. We can notice that we are printing results in exactly the same sequence in which we had given coroutine as input to gather() method.

import asyncio
from datetime import datetime
import time

async def addition(a,b):
    await asyncio.sleep(3)
    return "Addition", a + b

async def multiplication(a,b):
    await asyncio.sleep(1)
    return "Multiplication", a * b

async def division(a,b):
    await asyncio.sleep(5)
    return "Division", a / b

async def subtraction(a,b):
    await asyncio.sleep(7)
    return "Subtract", a - b

async def main():

    corrs_result  = await asyncio.gather(division(10,20),
                         subtraction(10,20),
                         addition(10,20),
                         multiplication(10,20))

    for task, result in corrs_result:
        print("{} : {}".format(task, result))


print("Start Time : ", datetime.now(), "\n")
start = time.time()

asyncio.run(main())

print("\nEnd   Time : ", datetime.now())
print("\nTotal Time Taken : {} Seconds".format(time.time() - start))

OUTPUT

Start Time :  2021-02-21 10:37:35.488688

Division : 0.5
Subtract : -10
Addition : 30
Multiplication : 200

End   Time :  2021-02-21 10:37:42.492182

Total Time Taken : 7.003508567810059 Seconds

4. Retrieve Coroutines Results as They Complete using "as_completed()" Method

As a part of our fourth example, we'll demonstrate how we can wait for the list of awaitables and returns awaitables which has completed running using as_completed() method.


> Important Methods of 'asyncio' Module

  • as_completed(awaitables, timeout=None) - This method accepts a list of awaitables and returns an iterator of coroutines that can be awaited to get the fastest result from the list of pending awaitables. We can also give timeout parameter specifying to time-out function after specified seconds. It won't run any pending awaitables once timed out.

Our code for this example uses the same four arithmetic coroutines as the example previous example. Our main coroutine has two-part.

The first part wraps a call to all four arithmetic coroutines inside of as_completed() function as a list. It then awaits for the coroutine returned by the method one by one and prints the results as coroutines complete.

As we have awaited all coroutines, it'll return as coroutines complete and prints results which will be in sequence in which they complete based on the time taken by them.

The second part of the code is the same as the first part with two minor changes. T

he first change is that there is a timeout of 4 seconds introduced inside of as_completed() function which will timeout after 4 seconds and won't execute any pending coroutines/tasks.

The second change is that call to as_completed() is wrapped inside of the try-except block to catch and print error.

When we run the below script, we can notice from the output that the first part runs successfully whereas the second part fails after the coroutine addition and multiplication have completed because both took less than 4 seconds to execute and ran in parallel. It took 11 seconds for the code to run (7 seconds for the first part + 4 seconds for the second part).

import asyncio
from datetime import datetime
import time

async def addition(a,b):
    await asyncio.sleep(3)
    return "Addition", a + b

async def multiplication(a,b):
    await asyncio.sleep(1)
    return "Multiplication", a * b

async def division(a,b):
    await asyncio.sleep(5)
    return "Division", a / b

async def subtraction(a,b):
    await asyncio.sleep(7)
    return "Subtract", a - b


async def main():
    ##### PART 1
    for cor in asyncio.as_completed([division(10,20), subtraction(10,20), addition(10,20), multiplication(10,20)]):
        task, result = await cor
        print("{} : {}".format(task, result))

    print("\n", "="*80, "\n")

    ##### PART 2 : With Timeout
    try:
        for cor in asyncio.as_completed([division(10,20), subtraction(10,20), addition(10,20), multiplication(10,20)],
                                        timeout=4):
            task, result = await cor
            print("{} : {}".format(task, result))
    except Exception as e:
        print("Error Type {}, Error : {}".format(type(e), e))

print("Start Time : ", datetime.now(), "\n")
start = time.time()

asyncio.run(main())

print("\nEnd   Time : ", datetime.now())
print("\nTotal Time Taken : {} Seconds".format(time.time() - start))

OUTPUT

Start Time :  2021-02-21 10:41:55.551136

Multiplication : 200
Addition : 30
Division : 0.5
Subtract : -10

 ================================================================================

Multiplication : 200
Addition : 30
Error Type <class 'concurrent.futures._base.TimeoutError'>, Error : 

End   Time :  2021-02-21 10:42:06.557828

Total Time Taken : 11.006727695465088 Seconds

5. Make Coroutine Wait for Other Coroutines using "wait()" Method

As a part of our fifth example, we'll explain how we can make a task/coroutine wait for other task/tasks to complete using wait() method of asyncio module.


> Important Methods of 'asyncio' Module

  • wait(tasks, timeout=None, return_when=ALL_COMPLETED) - This method accepts a list of tasks and runs them in parallel until the condition specified by return_when parameter has been met. It returns two values as output where the first value is a list of completed task instances and the second value is a list of pending tasks instanced. We can also give timeout parameter specifying to time-out function after specified seconds. It won't run any pending tasks once timed out and return them as pending tasks. It won't cancel pending tasks though. The return_when parameter accepts one of the below values.
    • ALL_COMPLETED - It hints at the completion of all tasks.
    • FIRST_COMPLETED - It hints at the completion of the first task.
    • FIRST_EXCEPTION - It hints at the first exception in any task.

Please make a NOTE that wait() method accepts a list of tasks as input and not coroutines. It's deprecated to pass coroutines. Hence we should first create a task around coroutine and pass it to the method.


Our code for this example has exactly the same four arithmetic coroutines that we have been using for the last many examples. The code for the main coroutine has two parts.

The first part creates four tasks for each arithmetic coroutines. It then passes all tasks to wait() method and awaits their completion. By default, wait() method waits for the completion of all of them.

It then prints the count of completed tasks, completed tasks results and pending tasks count. We have retrieved task completion results by calling result() method of the Task instance.

The second part of the code has the same settings as that of the first part with two changes.

The wait() method has been given a timeout of 4 seconds. This will force the wait method to return after 4 seconds and returns tasks which are done and the ones which are pending.

The other change is that we have wrapped the code of the second part inside of the try-except block to capture any errors.

When we run the script, we can notice from the output that the first part of the code completes by running all tasks given to wait() method whereas the second part returns after completion of two tasks return other two tasks as pending.

import asyncio
from datetime import datetime
import time

async def addition(a,b):
    await asyncio.sleep(3)
    return "Addition", a + b

async def multiplication(a,b):
    await asyncio.sleep(1)
    return "Multiplication", a * b

async def division(a,b):
    await asyncio.sleep(5)
    return "Division", a / b

async def subtraction(a,b):
    await asyncio.sleep(7)
    return "Subtract", a - b


async def main():
    ######### PART 1
    task1, task2, task3, task4 = asyncio.create_task(division(10,20)), \
                                 asyncio.create_task(subtraction(10,20)), \
                                 asyncio.create_task(addition(10,20)), \
                                 asyncio.create_task(multiplication(10,20))

    completed, pending = await asyncio.wait([task1, task2, task3, task4])

    print("Completed Tasks : {}".format(len(completed)))
    for complete_task in completed:
        result = complete_task.result()
        print("{} : {}".format(*result))

    print("\nPending Tasks : {}".format(len(pending)))
    print("\n", "="*80, "\n")

    ############# PART 2 : With timeout
    task1, task2, task3, task4 = asyncio.create_task(division(10,20)), \
                                 asyncio.create_task(subtraction(10,20)), \
                                 asyncio.create_task(addition(10,20)), \
                                 asyncio.create_task(multiplication(10,20))
    try:
        completed, pending = await asyncio.wait([task1, task2, task3, task4], timeout=4)

        print("Completed Tasks : {}".format(len(completed)))
        for complete_task in completed:
            result = complete_task.result()
            print("{} : {}".format(*result))

        print("\nPending Tasks : {}".format(len(pending)))
        for pending_task in pending:
            print(pending_task.get_coro().__name__)

    except Exception as e:
        print("Error Type {}, Error : {}".format(type(e), e))

print("Start Time : ", datetime.now(), "\n")
start = time.time()

asyncio.run(main())

print("\nEnd   Time : ", datetime.now())
print("\nTotal Time Taken : {} Seconds".format(time.time() - start))

OUTPUT

Start Time :  2021-02-21 11:16:12.983014

Completed Tasks : 4
Division : 0.5
Addition : 30
Subtract : -10
Multiplication : 200

Pending Tasks : 0

 ================================================================================

Completed Tasks : 2
Multiplication : 200
Addition : 30

Pending Tasks : 2
division
subtraction

End   Time :  2021-02-21 11:16:23.989316

Total Time Taken : 11.006330966949463 Seconds

6. Make Coroutine Wait for Other Coroutines using "wait_for()" Method

As a part of our sixth example, we'll explain how we can make coroutine wait for another coroutine to complete using wait_for() method of the asyncio module.


> Important Methods of 'asyncio' Module

  • wait_for(awaitable, timeout) - This method accepts awaitable and timeout value as input and makes coroutine in which this method is called wait for the awaitable given as input to this method for a number of seconds given as timeout parameter value.

Our code for this example has used addition function from the previous example.

It has created a new coroutine named Raise which accepts 3 compulsory parameters and one optional parameter. It wraps call to addition coroutine on first two parameters inside wait_for() method.

The timeout parameter given to Raise is passed to wait_for(). The Raise returns the result of addition coroutine raised to third parameter p.

We have wrapped the code involving wait_for() method inside of the try-except block to catch any exception and set results to 0.

Our main coroutine calls await Raise coroutine with different parameters. The rest of the code is the same as the previous example which executes the main coroutine using run() method of asyncio and captures the time taken.

When we run the script, we can notice that the first call and last call to Raise coroutine succeeds because the first one waits forever and the third one waits for 4 seconds which is more than the time taken by addition coroutine (3 seconds).

The seconds call fails because it waits for 2 seconds and in that time, addition coroutine has not completed running because it takes 3 seconds to complete.

import asyncio
from datetime import datetime
import time

async def addition(a,b):
    await asyncio.sleep(3)
    return a + b

async def Raise(a, b, p, timeout = None):
    try:
        res = await asyncio.wait_for(addition(a, b), timeout=timeout)
        await asyncio.sleep(1)
    except Exception as e:
        print("Error Type : {}, Error : {}".format(type(e), str(e)))
        res = 0
    return res ** p

async def main():
    res = await Raise(10, 10, 2) ## Waits forever
    print("({} + {}) ^ {} = {}\n".format(10,10,2,res))

    res = await Raise(10, 10, 2, 2) ## Timeout after 2 seconds
    print("({} + {}) ^ {} = {}\n".format(10,10,2,res))

    res = await Raise(10, 10, 2, 4) ## Timeout after 4 seconds
    print("({} + {}) ^ {} = {}\n".format(10,10,2,res))



print("Start Time : ", datetime.now(), "\n")
start = time.time()

asyncio.run(main())

print("\nEnd   Time : ", datetime.now())
print("\nTotal Time Taken : {} Seconds".format(time.time() - start))

OUTPUT

Start Time :  2021-02-21 11:22:05.952169

(10 + 10) ^ 2 = 400

Error Type : <class 'asyncio.exceptions.TimeoutError'>, Error : 
(10 + 10) ^ 2 = 0

(10 + 10) ^ 2 = 400


End   Time :  2021-02-21 11:22:15.965098

Total Time Taken : 10.012948274612427 Seconds

7. Retrieve Current Task and All Tasks using "current_task()" and "all_task()" Methods

As a part of our seventh example, we are demonstrating methods of asyncio module and Task instance. We'll be explaining usage of current_task() and all_tasks() methods of asyncio module and result() and get_name() methods of Task instance.


> Important Methods of 'asyncio' Module

  • current_task() - This method returns Task instance for the task in which it's called.
  • all_tasks() - This method returns list of tasks which are not finished yet.

Our code for this example builds on code that we have used till now for various examples.

We have modified the definition of all arithmetic coroutines. We are getting Task instances inside of each coroutine and printing statements saying that the task has started execution.

We are then pausing tasks for few seconds (asyncio.sleep()) and returning the result.

Our main subroutine creates four tasks for four arithmetic coroutines.

It then prints a total number of tasks and their names using total_tasks() method of asyncio.

It then awaits all tasks for completion.

After all the tasks have completed, it again retrieves a list of pending tasks and prints their count as well as names.

At last, it prints the results of completed tasks.

The rest of the code is the same as the previous example which runs the main coroutine.

When we run the script, we can notice in the output that it prints pending tasks as 5 whereas we have only created 4 tasks. The reason behind this is because the main coroutine is also another task. When print task count after 4 arithmetic tasks have completed, it prints 1 task as pending which is the main coroutine. The Task-1 points to the main coroutine.

import asyncio
from datetime import datetime
import time

async def addition(a,b):
    curr_task = asyncio.current_task() ## Retrieve current task.
    print("{} Started.".format(curr_task.get_name()))
    await asyncio.sleep(3)
    return a + b

async def multiplication(a,b):
    curr_task = asyncio.current_task()
    print("{} Started.".format(curr_task.get_name()))
    await asyncio.sleep(1)
    return a * b

async def division(a,b):
    curr_task = asyncio.current_task()
    print("{} Started.".format(curr_task.get_name()))
    await asyncio.sleep(5)
    return a / b

async def subtraction(a,b):
    curr_task = asyncio.current_task()
    print("{} Started.".format(curr_task.get_name()))
    await asyncio.sleep(7)
    return a - b


async def main():

    div_task = asyncio.create_task(division(10,20), name="Division")

    subtract_task = asyncio.create_task(subtraction(10,20), name="Subtraction")

    add_task = asyncio.create_task(addition(10,20), name="Addition")

    mul_task = asyncio.create_task(multiplication(10,20), name="Multiplication")

    total_tasks = asyncio.all_tasks()
    print("Total Tasks ({}) : {}\n".format(len(total_tasks), [task.get_name() for task in total_tasks]))

    await div_task
    await subtract_task
    await add_task
    await mul_task

    total_tasks = asyncio.all_tasks()
    print("\nTotal Tasks ({}) : {}\n".format(len(total_tasks), [task.get_name() for task in total_tasks]))

    print("\nAddition Result       : ", add_task.result())
    print("Multiplication Result : ", mul_task.result())
    print("Division Result       : ", div_task.result())
    print("Subtraction Result    : ", subtract_task.result())

print("Start Time : ", datetime.now(), "\n")
start = time.time()

asyncio.run(main())

print("\nEnd   Time : ", datetime.now())
print("\nTotal Time Taken : {} Seconds".format(time.time() - start))

OUTPUT

Start Time :  2021-02-21 12:21:34.883234

Total Tasks (5) : ['Task-1', 'Division', 'Multiplication', 'Subtraction', 'Addition']

Division Started.
Subtraction Started.
Addition Started.
Multiplication Started.

Total Tasks (1) : ['Task-1']


Addition Result       :  30
Multiplication Result :  200
Division Result       :  0.5
Subtraction Result    :  -10

End   Time :  2021-02-21 12:21:41.888303

Total Time Taken : 7.0050740242004395 Seconds

8. Cancel Tasks (Coroutines) using "cancel()" Method

As a part of our eighth example, we are demonstrating how we can cancel an ongoing task using cancel() method of Task instance.


> Important Methods of Task Instance

  • cancel(message=None) - This method request cancellation of task. It throws CancelledError exception with the message give as input. The coroutine can even suppress CancelledError exception using try-catch block if it does not want the task to be canceled.
  • done() - This method returns True if task is completed else False.
  • cancelled() - This method returns True if the task is cancelled else False. It returns True only if the canceled task does not capture the raised error. If the canceled task has handled the error using try-except block and not have raised the error then it'll return False.
  • exception() - This method returns exception happened in the task.

Our code for this example builds on the previous example.

We have used four arithmetic coroutine from the previous example but we have wrapped their code inside of the try-except block to catch CancelledError exception.

We have raised the error again after printing its details so that caller of arithmetic coroutine can capture them again. This will make sure that cancelled() method of Task instance returns True when the task is canceled.

Our main coroutine starts by creating a task for each arithmetic coroutine.

It then prints a list of existing tasks and their names. We are then putting the main coroutine to sleep for 4 seconds.

After that, we are checking for a list of tasks about their completion using done() method of Task instance. If the task is completed then we print their name and if the task is pending then we cancel it with a message using cancel() method of Task instance.

We have also put a check for the main coroutine task so that we don't cancel our main task by mistake.

Our code then awaits completion for each task. The await statements are wrapped in try-except block to catch any error (CancelledError).

At last, we are printing the results of the tasks by checking whether the task was canceled or not. If the task was cancelled then we print the result as 0 else we print the result retrieved using result() method of Task.

When we run the script, we can notice from the output that after the main task has paused for 4 seconds, only addition and multiplication tasks have completed running. The division and subtraction tasks are pending and hence canceled. Both tasks prints cancellation message and then we see the result of tasks execution.

import asyncio
from datetime import datetime
import time

async def addition(a,b):
    try:
        curr_task = asyncio.current_task()
        print("{} Started.".format(curr_task.get_name()))
        await asyncio.sleep(3)
        return a + b
    except asyncio.CancelledError as e:
        print("\nTask : {} Cancelled. Exception {}. {}".format(asyncio.current_task().get_name(), type(e).__name__, str(e)))
        raise e

async def multiplication(a,b):
    try:
        curr_task = asyncio.current_task()
        print("{} Started.".format(curr_task.get_name()))
        await asyncio.sleep(1)
        return a * b
    except asyncio.CancelledError as e:
        print("\nTask : {} Cancelled. Exception {}. {}".format(asyncio.current_task().get_name(), type(e).__name__, str(e)))
        raise e

async def division(a,b):
    try:
        curr_task = asyncio.current_task()
        print("{} Started.".format(curr_task.get_name()))
        await asyncio.sleep(5)
        return a / b
    except asyncio.CancelledError as e:
        print("\nTask : {} Cancelled. Exception {}. {}".format(asyncio.current_task().get_name(), type(e).__name__, str(e)))
        raise e

async def subtraction(a,b):
    try:
        curr_task = asyncio.current_task()
        print("{} Started.".format(curr_task.get_name()))
        await asyncio.sleep(7)
        return a - b
    except asyncio.CancelledError as e:
        print("\nTask : {} Cancelled. Exception {}. {}".format(asyncio.current_task().get_name(), type(e).__name__, str(e)))
        raise e

async def main():

    div_task = asyncio.create_task(division(10,20), name="Division")

    subtract_task = asyncio.create_task(subtraction(10,20), name="Subtraction")

    add_task = asyncio.create_task(addition(10,20), name="Addition")

    mul_task = asyncio.create_task(multiplication(10,20), name="Multiplication")

    total_tasks = asyncio.all_tasks()
    print("Total Tasks ({}) : {}\n".format(len(total_tasks), [task.get_name() for task in total_tasks]))

    ## Sleeping main task for 4 seconds.
    await asyncio.sleep(4)

    print()
    for task in total_tasks:
        if task.done():
            print("{} Done.".format(task.get_name()))
        else:
            if task.get_name() != "Task-1":
                task.cancel("Took longer than 4 seconds to complete. Hence cancelling pending tasks.")
                print("{} Cancelled.".format(task.get_name()))

    try:
        await div_task
    except asyncio.CancelledError as e:
        #print("\nTask : {}. Exception {}. {}".format(div_task.get_name(), type(e).__name__, str(e)))
        pass

    try:
        await subtract_task
    except asyncio.CancelledError as e:
        #print("\nTask : {}. Exception {}. {}".format(subtract_task.get_name(), type(e).__name__, str(e)))
        pass

    try:
        await add_task
    except asyncio.CancelledError as e:
        #print("\nTask : {}. Exception {}. {}".format(add_task.get_name(), type(e).__name__, str(e)))
        pass

    try:
        await mul_task
    except asyncio.CancelledError as e:
        #print("\nTask : {}. Exception {}. {}".format(mul_task.get_name(), type(e).__name__, str(e)))
        pass


    print("\nAddition Result       : ", add_task.result() if not add_task.cancelled() else 0)
    print("Multiplication Result : ", mul_task.result() if not mul_task.cancelled() else 0)
    print("Division Result       : ", div_task.result() if not div_task.cancelled() else 0)
    print("Subtraction Result    : ", subtract_task.result() if not subtract_task.cancelled() else 0)

print("Start Time : ", datetime.now(), "\n")
start = time.time()

asyncio.run(main())

print("\nEnd   Time : ", datetime.now())
print("\nTotal Time Taken : {} Seconds".format(time.time() - start))

OUTPUT

Start Time :  2021-02-21 12:25:46.935120

Total Tasks (5) : ['Addition', 'Division', 'Multiplication', 'Task-1', 'Subtraction']

Division Started.
Subtraction Started.
Addition Started.
Multiplication Started.

Addition Done.
Division Cancelled.
Multiplication Done.
Subtraction Cancelled.

Task : Division Cancelled. Exception CancelledError. Took longer than 4 seconds to complete. Hence cancelling pending tasks.

Task : Subtraction Cancelled. Exception CancelledError. Took longer than 4 seconds to complete. Hence cancelling pending tasks.

Addition Result       :  30
Multiplication Result :  200
Division Result       :  0
Subtraction Result    :  0

End   Time :  2021-02-21 12:25:50.938051

Total Time Taken : 4.00295615196228 Seconds

This ends our small tutorial explaining the usage of (async, await) keywords, and asyncio module methods with simple and easy-to-understand examples. The 'asyncio' module has same API as that of Python threading and multiprocessing modules. Having background of those modules can make things easier to learn. We have included links to them below.

Sunny Solanki  Sunny Solanki

Share Views Stuck Somewhere? Need Help with Coding? Have Doubts About the Topic/Code?

When going through coding examples, it's quite common to have doubts and errors.

If you have doubts about some code examples or are stuck somewhere when trying our code, send us an email at coderzcolumn07@gmail.com. We'll help you or point you in the direction where you can find a solution to your problem.

You can even send us a mail if you are trying something new and need guidance regarding coding. We'll try to respond as soon as possible.

Share Views Want to Share Your Views? Have Any Suggestions?

If you want to

  • provide some suggestions on topic
  • share your views
  • include some details in tutorial
  • suggest some new topics on which we should create tutorials/blogs
Please feel free to contact us at coderzcolumn07@gmail.com. We appreciate and value your feedbacks. You can also support us with a small contribution by clicking DONATE.