For a general overview of the advantages and concepts behind async patterns, check out:
With Python’s asyncio
library you can write concurrent code for I/O-bound strategies. This differs from the multiprocessing
library which focuses on using threads for CPU-bound strategies.
The asyncio
library along with the async
/await
keywords enable this by using a concept called the "Event Loop". This provides an abstract way to create and schedule asynchronous loops and functions. JavaScript, being used in front-end interaction in browsers, is also based around the event loop concept making it a perfect language to compare/examine this concept.
Note: Some of the
asyncio
functions implemented in Python 3.7+ may beDeprecated
in Python 3.10+
Synchronous Execution
Normally when programming in Python and most other Procedural languages, programs are executed from top to bottom (even if the code is not structured as such; see language interpretation/compilers). This means that each statement is executed after the completion of the prior statement(s).
In example, say we have an random_add
function that sums a list of random integers after a specified delay
(in seconds):
def random_add(delay: int) -> None:
import time
time.sleep(delay)
# This retrieves a list of random ints
n = random_number_list()
print(f'Sum({n}) = {sum(n)}')
and we execute this function 3 times in succession:
if __name__ == "__main__":
random_add(5) # call1: Delayed 5s
random_add(8) # call2: Delayed 8s
random_add(2) # call3: Delayed 2s
the total timing would be near 15s. With each call completing one after the next as shown below.
Even though the random_add
function is not performing any calculations during its delayed period, it still has resources allocated for that period. This is why the time consumption of normal synchronous programs can typically be described as the sum of all of its procedures.
Synchronous Timing
call | start | finish | finish order |
---|---|---|---|
1 | 0s | 5s | 1 |
2 | 5s | 13s | 2 |
3 | 13s | 15s | 3 |
Asynchronous Execution
We can reuse the prior random_add
function and adapt it to execute asychronously:
import asyncio
async def random_add(delay: int) -> None:
asyncio.sleep(delay)
# This retrieves a list of random ints
n = random_number_list()
print(f'Sum({n}) = {sum(n)}')
now to execute this function 3 times just as before:
if __name__ == "__main__":
await random_add(5)
await random_add(8)
await random_add(2)
Uh Oh! You should receive a SyntaxError
similar to:
await random_add(5)
^^^^^^^^^^^^^^^^^^^
SyntaxError: 'await' outside function
This issue may have even been detected by the IDE you are using before running the Python script. This is because await
can only be used in async
scopes and the main
thread is synchronous without an event loop! To execute this code we must:
-
Wrap this code in a
async
function:
async def async_test(): await random_add(5) await random_add(8) await random_add(2)
-
Then utilize the
asyncio.run
function
if __name__ == "__main__": asyncio.run(async_test())
Now the code should run happily. However, the code is still taking the same amount of time to execute. Why is that? Doesn't async
make it work in parallel?
The reason is that the await
keyword means the parent function will await a result before continuing to the next statement. Since the response is delayed it will wait for that period.
To execute the random_add
function calls concurrently, we need to create Task
s to execute them in. This can be done via asyncio.create_task
individually:
async def async_test_task():
task1 = asyncio.create_task(random_add(5))
task2 = asyncio.create_task(random_add(8))
task3 = asyncio.create_task(random_add(2))
await task1, await task2, await task3
or via asyncio.gather
for lists of tasks:
async def async_test_gather():
await asyncio.gather(
random_add(5),
random_add(8),
random_add(2)
)
For the rest of this post we will be utilizing asyncio.gather
for simplicity. There are certain scenarios (eg. variable task creation) that asyncio.create_task
is applicable.
Now that the code has been adjusted you will observe that the total execution time is only limited by the slowest task! Also, the order each task completes differs (shown below).
Asynchronous Timing
call | start | finish | finish order |
---|---|---|---|
1 | 0s | 5s | 2 |
2 | ~0s | 8s | 3 |
3 | ~0s | 2s | 1 |
This is just the start of the features that are provided by async
/await
and the asyncio
library in Python. Be sure to read Part 2 of this overview once it is released.
Top comments (0)