The async
and await
keywords are used to manage asynchronous tasks.
Asynchronous programming allows multiple tasks to be processed simultaneously without blocking other tasks. In Python, it is achieved through the use of coroutines.
The async
keyword is used to define a function as an asynchronous coroutine, and the await
keyword is used inside of a coroutine function to wait for the execution of another coroutine.
What are coroutines?
Coroutines are just like regular functions with the additional capability of pausing and resuming their execution at specific points. This allows for non-blocking behavior, where the execution of a coroutine can be suspended in case of delays or to deliberately allow other code to run before resuming its execution.
Coroutine are very closely related to generator functions. In fact, prior to the introduction of async
and await
in Python 3.5, generator functions were used in a special way to implement coroutines.
With python 3.5 and later versions, we can now create coroutines more conveniently and natively using the two keywords, async
and await
.
async
creates a coroutine function.await
suspends a coroutine to allow another coroutine to be executed.
Define coroutine functions with async
Defining coroutine functions is much like defining regular functions. Consider the following example:
Define a regular function
def add(a, b):
print(f'{a} + {b} = {a + b}')
With a regular function, we use the def
keyword in the declaration, with a coroutine function, we use async def
instead of just def
.
Define a coroutine function
async def add(a, b):
print(f'{a} + {b} = {a + b}')
Calling a coroutine function
To call a regular function, you simply use its name followed by parenthesis with the necessary arguments.
Call a regular function
def add(a, b):
print(f'{a} + {b} = {a + b}')
add(10, 20)
Unlike with regular functions, calling a coroutine function does not immediately execute the code inside the function. Instead, it creates a coroutine object.
async def add(a, b):
print(f'{a} + {b} = {a + b}')
#call a coroutine
coro = add(10, 20) #creates a coroutine object
print(coro)
To actually run the code inside the coroutine function, you need to await it in what is referred to as an event loop. The most direct way to do this is by using the run()
function in the asyncio
module of the standard library.
import asyncio
async def add(a, b):
print(f'{a} + {b} = {a + b}')
#create a coroutine object
coro = add(10, 20)
#run the coroutine
asyncio.run(coro) #executes the code in the coroutine
The asyncio.run()
function creates an event loop, runs the specified coroutine and returns the result of the coroutine. It is designed to be used for running the main function of an asynchronous application or to run a simple one-off coroutine.
Without asyncio.run()
you would be needed to manually, initialize and manage the event loop, create and schedule tasks, and wrap the code in a coroutine. This would be more complicated and error-prone compared to using asyncio.run()
, which handles all of these steps automatically.
more examples
import asyncio
async def add(a, b):
print(f'{a} + {b} = {a + b}')
asyncio.run(add(50, 30))
asyncio.run(add(50, 50))
asyncio.run(add(90, 10))
Suspending coroutines with await
The await
keyword is used to pause the execution of a coroutine until another inner coroutine is finished. Once the awaited coroutine has finished, the original coroutine will resume its execution from where it left off.
await <coro()>
The await
keyword takes a single argument, coro()
, a coroutine object to be awaited.
Consider the following example.
await a coroutine
import asyncio
#prints even numbers from 0-n
async def display_evens(n):
for i in range(n):
if i % 2 == 0:
print(i)
async def main():
print("Started!")
await display_evens(10) #await display_evens()
print('Finished!')
#run main
asyncio.run(main())
In the above example, when the await
statement is encountered, the execution of main is suspended until the display_evens()
coroutine is fully executed. Multiple coroutines can be awaited as necessary.
await multiple coroutines
import asyncio
#prints even numbers from 0-n
async def display_evens(n):
for i in range(n):
if i % 2 == 0:
print(i)
await asyncio.sleep(0.01)#cause a small delay
#prints odd numbers from 0-n
async def display_odds(n):
for i in range(n):
if i % 2 == 1:
print(i)
await asyncio.sleep(0.01)#cause a small delay
async def main():
print("Started!")
await display_evens(10)
await display_odds(10)
print('Finished!')
#run main
asyncio.run(main())
In the above example, the awaited coroutines, display_evens()
and display_odds()
are executed one after the other. In the following section, we will see how to make awaited coroutines to be executed simultaneously.
Executing awaited coroutines in parallel.
The primary goal of asynchronous programming is to execute multiple tasks in parallel. This means that while one task is executing, another task can start and run simultaneously.
To run multiple coroutines in parallel and in a non-blocking manner, we can combine them into a single awaitable object using the asyncio.gather()
function, then await the returned object.
execute multiple coroutines
import asyncio
#prints even numbers from 0-n
async def display_evens(n):
for i in range(n):
if i % 2 == 0:
print(i)
await asyncio.sleep(0.01)#cause a small delay
#prints odd numbers from 0-n
async def display_odds(n):
for i in range(n):
if i % 2 == 1:
print(i)
await asyncio.sleep(0.01)#cause a small delay
async def main():
print("Started!")
await asyncio.gather(display_evens(10), display_odds(10)) #gathered coroutines will run in parallel
print('Finished!')
#run main
asyncio.run(main())
If you observe the above output, you will see that both display_evens()
and display_odds()
are executed simultaneously. The outputs from the two functions are emitted alternately making it look as if it was just a single function printing all the integers.
Conclusion
- Asynchronous programming makes it possible to execute tasks in parallel.
- A coroutine is just like a regular function except that it is capable of pausing its execution and resuming later on at the same point.
- The
async
keyword creates coroutine objects. - The
await
keyword suspends the execution of a coroutine until an inner coroutine is executed fully. - The
asyncio.run()
function eases event loop management. It executes a single coroutine. - The
asyncio.gather()
function, combines multiple coroutines into a single awaitable object.