Simplest async/await example possible in Python
Solution 1
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:
async def sleep():
print(f'Time: {time.time() - start:.2f}')
await asyncio.sleep(1)
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 and 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():
...
time.sleep(1)
# case 2
async def sleep():
...
time.sleep(1)
# case 3
async def sleep():
...
await asyncio.sleep(1)
In case 1 and case 2, they are the "same": they "sleep" without allowing others to use the resources. Whereas in case 3, it 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 say 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? Right here!
await asyncio.sleep(1)
More on this read here
Update 02/May/2020
Consider reading
Solution 2
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 otherasyncio
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.
Solution 3
Python 3.7+ now has a simpler API (in my opinion) with a simpler wording (easier to remember than "ensure_future"): you can use create_task
which returns a Task object (that can be useful later to cancel the task if needed).
Basic example 1
import asyncio
async def hello(i):
print(f"hello {i} started")
await asyncio.sleep(4)
print(f"hello {i} done")
async def main():
task1 = asyncio.create_task(hello(1)) # returns immediately, the task is created
await asyncio.sleep(3)
task2 = asyncio.create_task(hello(2))
await task1
await task2
asyncio.run(main()) # main loop
Result:
hello 1 started
hello 2 started
hello 1 done
hello 2 done
Basic example 2
If you need to get the return value of these async functions, then gather
is useful. The following example is inspired from the documentation, but unfortunately the doc doesn't show what gather
is really useful for: getting the return values!
import asyncio
async def factorial(n):
f = 1
for i in range(2, n + 1):
print(f"Computing factorial({n}), currently i={i}...")
await asyncio.sleep(1)
f *= i
return f
async def main():
L = await asyncio.gather(factorial(2), factorial(3), factorial(4))
print(L) # [2, 6, 24]
asyncio.run(main())
Expected output:
Computing factorial(2), currently i=2...
Computing factorial(3), currently i=2...
Computing factorial(4), currently i=2...
Computing factorial(3), currently i=3...
Computing factorial(4), currently i=3...
Computing factorial(4), currently i=4...
[2, 6, 24]
PS: even if you use asyncio
, and not trio
, the tutorial of the latter was helpful for me to grok Python asynchronous programming.
Solution 4
Since everything is nicely explained, then let's run some examples with event loops compare synchronous code to asynchronous code.
synchronous code:
import time
def count():
time.sleep(1)
print('1')
time.sleep(1)
print('2')
time.sleep(1)
print('3')
def main():
for i in range(3):
count()
if __name__ == "__main__":
t = time.perf_counter()
main()
t2 = time.perf_counter()
print(f'Total time elapsed: {t2:0.2f} seconds')
output:
1
2
3
1
2
3
1
2
3
Total time elapsed: 9.00 seconds
We can see that each cycle of count running to completion before the next cycle begins.
asynchronous code:
import asyncio
import time
async def count():
await asyncio.sleep(1)
print('1')
await asyncio.sleep(1)
print('2')
await asyncio.sleep(1)
print('3')
async def main():
await asyncio.gather(count(), count(), count())
if __name__ == "__main__":
t = time.perf_counter()
asyncio.run(main())
t2 = time.perf_counter()
print(f'Total time elapsed: {t2:0.2f} seconds')
output:
1
1
1
2
2
2
3
3
3
Total time elapsed: 3.00 seconds
The asynshonous equivalent on the other hand looks somting like this took three seconds to run as opposed to nine secounds.
The first count cycle was started and as soon as it hit the await
s sleep one Python was free to do other work, for instance starting the secound and subsequently the third count cycles.
This is why we have all the ones than all tubes then all three.
In the output programing concurrently can be a very valuable tool.
Multiprocessing has the operating do all of the multitasking work and in Python it's the only option for multi-core concurrency that is having your program executed on multiple cores of CPU.
If use threads then the operating system is still doing all of the multitasking work and in cpython the global intrepeter lock prevents multi-core concurrency in asynshonous programming.
There is no operating system intervention there's one process there's one thread so what's going on well tasks can release the CPU when there are waiting periods, so that other task can use it.
import asyncio
loop = asyncio.get_event_loop()
async def greeter(name):
print(f"Hi, {name} you're in a coroutine.")
try:
print('starting coroutine')
coro = greeter('LP')
print('entering event loop')
loop.run_until_complete(coro)
finally:
print('closing event loop')
loop.close()
output:
starting coroutine
entering event loop
Hi, LP you're in a coroutine.
closing event loop
Asynchronous frameworks need a scheduler usually called an event loop. This event loop keeps track of all the running tasks and when a function suspended it returns control to the event loop which then will find another function to start or resume and this is called cooperative multitasking. Async IO provides a framework an asynchronous framework that's centered on this event loop and it efficiently handles input/output events an application interacts with the event loop explicitly it registers code to be run and then it lets the event loop the scheduler make the necessary calls into application code when the resources are available. So, if a network server open sockets and then registers them to be told when input events occur on them the event loop will alert the server code when there's a new incoming connection or when there's data to be read. If there's no more data to be read from a socket than the server then yields control back to the event loop.
The mechanism from yielding control back to the event loop depends on co-routines co-routines are a language construct designed for concurrent operation. The co-routine can pause execution using the awake keyword with another co-routine and while it's paused the co-routine state is maintained allowing it to resume where it left off one co-routine can start another and then wait for the results and this makes it easier to decompose a task into reusable parts.
import asyncio
loop = asyncio.get_event_loop()
async def outer():
print('in outer')
print('waiting for result 1')
result1 = await phase1()
print('waiting for result 2')
result2 = await phase2(result1)
return result1, result2
async def phase1():
print('in phase1')
return 'phase1 result'
async def phase2(arg):
print('in phase2')
return 'result2 derived from {}'.format(arg)
asyncio.run(outer())
output:
in outer
waiting for result 1
in phase1
waiting for result 2
in phase2
This example asks two phases that must be executed in order but that can run concurrently with other operations. The awake
keyword is used instead of addingbthe new co-routines to the loop because control flow is already inside of a co-routine being managed by the loop. It isn't necessary to tell the loop to manage the new co-routines.
Solution 5
Simple..Sweet..Awesome.. ✅
import asyncio
import time
import random
async def eat():
wait = random.randint(0,3)
await asyncio.sleep(wait)
print("Done With Eating")
async def sleep():
wait = random.randint(0,3)
await asyncio.sleep(wait)
print("Done With Sleeping")
async def repeat():
wait = random.randint(0,3)
await asyncio.sleep(wait)
print("Done With Repeating")
async def main():
for x in range(5):
await asyncio.gather(eat(),sleep(),repeat())
time.sleep(2)
print("+","-"*20)
if __name__ == "__main__":
t = time.perf_counter()
asyncio.run(main())
t2 = time.perf_counter()
print(f'Total time elapsed: {t2:0.2f} seconds')
Basj
I work on R&D involving Python, maths, machine learning, deep learning, data science, product design, and MacGyver solutions to complex problems. I love prototyping, building proofs-of-concept. For consulting/freelancing inquiries : [email protected]
Updated on March 30, 2022Comments
-
Basj about 2 years
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 usesensure_future
, and for learning purposes about asynchronous programming in Python, I would like to see an even more minimal example, and what are the minimal tools necessary to do a basic async / await example.Question: is it possible to give a simple example showing how
async
/await
works, by using only these two keywords + code to run the async loop + other Python code but no otherasyncio
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. -
Mikhail Gerasimov almost 6 years@Basj, I see now :) I updated answer with more useful example. Hope it'll help. In real life everything will be same, except instead of
asyncio.sleep
you will use async implemented I/O operations. -
Basj almost 6 yearsThank you. So is it mandatory to use something like
gather
orensure_future
or similar things? -
Mikhail Gerasimov almost 6 years@Basj technically you can write async example without
gather
/ensure_future
and it will work (see very first code snippet in answer). But withoutgather
/ensure_future
you can't run coroutines concurrently (parallel) and respectively you can't get benefit of usingasyncio
at all. In other words, it's not mandatory, but there's just no sense in usingasyncio
withoutgather
/ensure_future
. -
Mikhail Gerasimov almost 6 years@Basj of course you can benefit just of awaiting some third-party coroutine if this coroutine parallelize things (using
gather
/ensure_future
) inside self implementation. -
Tristan Forward almost 4 yearsFor "gather" is it possible to move the values outside of async main? For example: instead of print(L) we have return L. For me it just returns None. Or must they remain with the async function?
-
Nico Haase over 3 yearsPlease add some explanation to your answer such that others can learn from it
-
Ruman over 3 yearsif you run this simple program then watching the output definitely you can understand how everything is working . As this program is so simple to understand that's why i didn't add any explanation . Later i will try to add explanation along with code. Thank you
-
quickinsights about 3 yearsYou might also want to mention that the sleep statement would typically represents an IO operation
-
AsheKetchum about 3 yearswouldn't say useless but the link is nice
-
skytree over 2 yearsWhat if there is a dependence between async function and the following computation?
-
Piyapan Poomsirivilai about 2 yearsvery nice. It would be great if this is in the official documentation.
-
Timo about 2 yearsNice - what if in example 1 the sleeptimes in both are similar? To make example 1 clearer you should add an example without
async
. Without async playing with sleeptimes does not matter whereas with async it does. So there is concurrency race.. -
Timo about 2 years@quickinsights io in sleep because you input ms and get sleep as output?
-
Timo about 2 yearsWhy do you need
t
? Does not make sense,t2
does a diff of start and end of the script. -
Timo about 2 yearsSo you want to show that a date itself is fast because of parallelism with
asncio.sleep
whereas the break between days is slow withtime.sleep
? -
Rahul kuchhadia about 2 years@Timo Its for total time calculations
-
Michael Z. almost 2 years@Ruman one question though. In c# this line would be valid
response = fetch_users()
- Do you really need to callasyncio.create_task()
? I ask because the method is already defined as async. Typically you would wrap a non-async method in a task creation to make it async. Are you nesting a task unnecessarily? -
Ruman almost 2 years@MichaelZ. You are right. As fetch_users() function is already an async function that's why asyncio.create_task() is redundant. SImply we can use response = fetch_users().