The primary goal of asynchronous programming is to enable the execution of tasks in a non-blocking manner. This is achieved by moving from task to task until all tasks are fully executed. Shifting from one task is normally triggered if a delay is encountered on the currently executing task, the task with the delay gets paused and rescheduled for later continuation, then the execution moves to the next task in the schedule. This ensures that long-running tasks or those that may face delays will not stop other tasks from being executed.
There are two pproaches for executing multiple non-blocking tasks:
- using
asyncio.gather()
- using
asyncio.create_task()
Execute Multiple tasks with asyncio.gather()
The asyncio.gather()
function groups multiple tasks/coroutines for execution in a non-blocking manner.
The function has the following basic syntax:
asyncio.gather(*tasks, return_exceptions = False)
Where *tasks
parameter represents one or more tasks to be executed, tasks are usually coroutines but they can also be any other awaitable objects such as Task
or Future
objects.
The gather()
function returns a Future
object which is itself awaitable.
In the above example, we defined two coroutine to represent the asynchronous tasks. One coroutine displays numbers while the other displays letters. We used the asyncio.sleep()
function to simulate a small delay which when encountered causes the execution to move from one coroutine to the other. The result is that the two coroutines are executed alternately as proved by the outputs.
The gather()
function returns a Future
object as proved by the first line of the output. When the Future
object is awaited, the grouped tasks are executed asynchronously. When a delay, is encountered in one task, the task is paused and marked for continuation while the next coroutine in the schedule gets executed.
Our previous tasks did not return any values, however if a coroutine has a return values, we can access the values as a result of the await
expression. The return values are returned in a list in the order that the tasks are given.
Let us consider an example where the awaited tasks have return values.
get return values
In the above example, we have a coroutine square
, which returns the square of the number given as input. We grouped multiple calls to square
as tasks in the gather()
function. As shown in the outputs, the return value of the awaited operation is a list of the values returned by the tasks in the order that the task are entered.
By default, if an exception is raised in a running coroutine, the execution of all tasks is halted and the exception is propagated to the callers, but if the return_exceptions
parameter is set to True
, the execption is inserted as a value in place of that particular coroutine's return value and the execution continues with the remaining coroutines.
In the above example, the call to square('ten')
raises a TypeError
exception. When the return_exception
is not set to True
, the exception is raised normally and the program immediately stops. However, when return_exceptrions
set to True
as in second snippet, the raised exception is captured and inserted in place of the involved coroutine's return value in the returned list.
Execute Multiple tasks with asyncio.create_task()
A Task
object wraps a coroutine and schedules it for execution as soon as possible in the event loop.
The scheduled task gets executed in a non-blocking manner, this means that the execution can shift to another task if a delay is encountered or the task takes too long.
The asyncio.create_task()
function creates a Task
object for the coroutine given as argument.
As you can see from the above example, the two tasks are being executed simultaneously as proved by the alternating numbers and letters in the output. The small delay caused by asyncio.sleep()
calls is enough to make the execution to alternately switch between the two tasks.
Note that while it is not necessary to await the tasks in order for their execution to commence, it is important to await them at some point as this ensures that a task is fully executed. Await
statements tells the execution to ensure that the awaited task/coroutine has been executed fully before closing the event loop.
Without awaiting a task, the execution may be started but never finished, consider the following example where we do not await the tasks.
As shown above, the execution of either task is started but never finished. When a delay is encountered in a task, the execution simply moves on and does not return to that task.
It is also important to await the tasks if we are expecting outputs from the wrapped coroutines. The return value of the await expression is the return value of the coroutine.
Conclusion
- We can use either
asyncio.gather()
orasyncio.create_task()
functions to execute multiple asynchronous tasks in a non-blocking manner. - The non-blocking mode of execution, ensures that a long running task or one facing delays does not block the execution of other tasks.
- The
asyncio.gather()
function groups multiple tasks in to aFuture
object. When this object is awaited, the grouped tasks gets executed in a non-blocking manner. - The
asyncio.create_task()
function wraps a coroutine in aTask
object. A task object schedules the wrapped coroutine for execution as soon as possible in the event loop. - It is important to await the tasks to ensure that the will be fully executed.