Updated On : Feb-23,2021 Tags asyncio, async-await, concurrent_programming
asyncio - Concurrent Programming using Async/Await Syntax in Python

asyncio - Concurrent Programming using Async/Await Syntax in Python

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 a 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 when 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 giving up CPUs when they don't need it.

Async/Await programming is generally referred to as cooperative multitasking or non-preemptive multitasking as well 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 itself 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.

Python provides support for Async/Await programming using async, await keywords, and asyncio module. 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. We can then use await keyword which will hint Python interpreter that the statement 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.

As a part of this tutorial, we'll explain how we can write asynchronous code in Python using async/await syntax. We'll also explain how we can use asyncio module to run asynchronous code in parallel. Our first two examples just explain how we can create asynchronous code using async/await syntax and run it. It does not run code actually in parallel. The examples starting from the third example runs the code in parallel.

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.

Example 1

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.

  • wait_seconds() - It makes the task wait for that many number seconds.
  • 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 function that waits for a specified number of seconds.

Our main function is another asynchronous function that awaits all arithmetic function completion. We have called all functions with different arguments.

We are then running our asynchronous main function using run() method of asyncio module. It executes a coroutine function.


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.

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 is because we have not created tasks out of functions that are eligible to run in parallel. We'll explain how to create tasks to run this code in parallel in example 3.

In [ ]:
import asyncio
from datetime import datetime
import time

async def wait_seconds(seconds):
    time.sleep(seconds)

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

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

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

async def subtraction(a,b):
    await wait_seconds(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 09:49:11.558937

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

End   Time :  2021-02-21 09:49:27.577227

Total Time Taken : 16.0182945728302 Seconds

Example 2

Our second example is exactly the same as our first example with a minor change in code. We have removed wait_seconds() function which uses time module. We have instead used sleep() function of asyncio module to instruct the coroutine to sleep for that many seconds. The reason behind using this function is that it'll give up CPU when not using for that many seconds whereas time.sleep() function won't do it.

Our output for this example is the same as the previous example because we still have not created tasks to run coroutines in parallel which we are going to explain in the next example. This example also takes 16 seconds to complete taking full time for each coroutine.


Important Methods of asyncio Module

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

In [ ]:
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

Example 3

As a part of our third 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. We are then executing our main coroutine like previous examples.

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.


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

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.

In [ ]:
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

Example 4

As a part of our fourth 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.

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

In [ ]:
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

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

In [ ]:
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

Example 5

As a part of our fifth 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. The 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).

In [ ]:
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
    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

Example 6

As a part of our sixth 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 call has the same settings as that of the first part with two changes. The wait() method has been given a timeout of 4 seconds. Which 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.

In [ ]:
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
    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

Example 7

As a part of our seventh 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 then sleeps for 1 second and 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 second 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.

In [ ]:
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)
    print("({} + {}) ^ {} = {}\n".format(10,10,2,res))

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

    res = await Raise(10, 10, 2, 4)
    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

Example 8

As a part of our eight 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. After that, 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.

In [ ]:
import asyncio
from datetime import datetime
import time

async def addition(a,b):
    curr_task = asyncio.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

Example 9

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

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.

In [ ]:
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. Please feel free to let us know your views in the comments section.



Sunny Solanki  Sunny Solanki