I've read many examples, blog posts, questions/answers about asyncio
/ async
/ await
in Python 3.5+, many were complex, the simplest I found was probably this one. Still it uses ensure_future
, and for learning purposes about asynchronous programming in Python, I would like to see if an even more minimal example is possible (i.e. what are the minimum tools necessary to do a basic async / await example).
Question: for learning purposes about asynchronous programming in Python, is it possible to give a simple example showing how async
/ await
works, by using only these two keywords + asyncio.get_event_loop()
+ run_until_complete
+ other Python code but no other asyncio
functions?
Example: something like this:
import asyncio
async def async_foo():
print("async_foo started")
await asyncio.sleep(5)
print("async_foo done")
async def main():
asyncio.ensure_future(async_foo()) # fire and forget async_foo()
print('Do some actions 1')
await asyncio.sleep(5)
print('Do some actions 2')
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
but without ensure_future
, and still demonstrates how await / async works.
is it possible to give a simple example showing how async
/ await
works, by using only these two keywords + asyncio.get_event_loop()
+
run_until_complete
+ other Python code but no other asyncio
functions?
This way it's possible to write code that works:
import asyncio
async def main():
print('done!')
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
But this way it's impossible to demonstrate why you need asyncio.
By the way, why do you need asyncio
, not just plain code? Answer is - asyncio
allows you to get performance benefit when you parallelize I/O blocking operations (like reading/writing to network). And to write useful example you need to use async implementation of those operations.
Please read this answer for more detailed explanation.
Upd:
ok, here's example that uses asyncio.sleep
to imitate I/O blocking operation and asyncio.gather
that shows how you can run multiple blocking operations concurrently:
import asyncio
async def io_related(name):
print(f'{name} started')
await asyncio.sleep(1)
print(f'{name} finished')
async def main():
await asyncio.gather(
io_related('first'),
io_related('second'),
) # 1s + 1s = over 1s
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
Output:
first started
second started
first finished
second finished
[Finished in 1.2s]
Note how both io_related
started then, after only one second, both done.
To answer your questions I will provide 3 different solutions to the same problem.
case 1: just normal python
import time
def sleep():
print(f'Time: {time.time() - start:.2f}')
time.sleep(1)
def sum(name, numbers):
total = 0
for number in numbers:
print(f'Task {name}: Computing {total}+{number}')
sleep()
total += number
print(f'Task {name}: Sum = {total}\n')
start = time.time()
tasks = [
sum("A", [1, 2]),
sum("B", [1, 2, 3]),
]
end = time.time()
print(f'Time: {end-start:.2f} sec')
output:
Task A: Computing 0+1
Time: 0.00
Task A: Computing 1+2
Time: 1.00
Task A: Sum = 3
Task B: Computing 0+1
Time: 2.01
Task B: Computing 1+2
Time: 3.01
Task B: Computing 3+3
Time: 4.01
Task B: Sum = 6
Time: 5.02 sec
case 2: async/await done wrong
import asyncio
import time
async def sleep():
print(f'Time: {time.time() - start:.2f}')
time.sleep(1)
async def sum(name, numbers):
total = 0
for number in numbers:
print(f'Task {name}: Computing {total}+{number}')
await sleep()
total += number
print(f'Task {name}: Sum = {total}\n')
start = time.time()
loop = asyncio.get_event_loop()
tasks = [
loop.create_task(sum("A", [1, 2])),
loop.create_task(sum("B", [1, 2, 3])),
]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
end = time.time()
print(f'Time: {end-start:.2f} sec')
output:
Task A: Computing 0+1
Time: 0.00
Task A: Computing 1+2
Time: 1.00
Task A: Sum = 3
Task B: Computing 0+1
Time: 2.01
Task B: Computing 1+2
Time: 3.01
Task B: Computing 3+3
Time: 4.01
Task B: Sum = 6
Time: 5.01 sec
case 3: async/await done right (same as case 2 except the sleep
function)
import asyncio
import time
async def sleep():
print(f'Time: {time.time() - start:.2f}')
await asyncio.sleep(1)
async def sum(name, numbers):
total = 0
for number in numbers:
print(f'Task {name}: Computing {total}+{number}')
await sleep()
total += number
print(f'Task {name}: Sum = {total}\n')
start = time.time()
loop = asyncio.get_event_loop()
tasks = [
loop.create_task(sum("A", [1, 2])),
loop.create_task(sum("B", [1, 2, 3])),
]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
end = time.time()
print(f'Time: {end-start:.2f} sec')
output:
Task A: Computing 0+1
Time: 0.00
Task B: Computing 0+1
Time: 0.00
Task A: Computing 1+2
Time: 1.00
Task B: Computing 1+2
Time: 1.00
Task A: Sum = 3
Task B: Computing 3+3
Time: 2.00
Task B: Sum = 6
Time: 3.01 sec
case 1
with case 2
give the same 5 seconds
, whereas case 3
just 3 seconds
. So the async/await done right
is faster.
The reason for the difference is within the implementation of sleep
function.
# case 1
def sleep():
print(f'Time: {time.time() - start:.2f}')
time.sleep(1)
# case 2
async def sleep():
print(f'Time: {time.time() - start:.2f}')
time.sleep(1)
# case 3
async def sleep():
print(f'Time: {time.time() - start:.2f}')
await asyncio.sleep(1)
sleep
function in case 1
and case 2
are the "same".
They "sleep" without allowing others to use the resources.
Whereas case 3
allows access to the resources when it is asleep.
In case 2
we added async
to the normal function. However the event loop will run it without interruption.
Why? Because we didn't tell where the loop is allowed to interrupt your function to run another task.
In case 3
we told the event loop exactly where to interrupt the function to run another task. Where exactly?
# case 3
async def sleep():
print(f'Time: {time.time() - start:.2f}')
await asyncio.sleep(1) # <-- Right here!
More on this read here