Asyncio Basics in Python
Python 3.5 brought with it asyncio, and is part of the standard library. There are other async libraries out there, but I am going to focus on the one built in to Python.
Creating an event loop
import asyncio
async def some_func():
print("Done sleeping")
await asyncio.sleep(1)
print("Done sleeping")
asyncio.run(some_func())
This is the basic example to get you up and running. This is the core of asyncio. Starting an event loop and running some function on top of it. Some frameworks abstract this from you and handle it on the bootstrap of the application layer (fastapi, for example).
The async function
async def some_func():
print("Done sleeping")
await asyncio.sleep(1)
print("Done sleeping")
An async function is like defining a regular function, except we add the async
keyword before def
. This not only let's you know the function is asynchronous, but it also let's the interpreter know that it’s a special function called a coroutine
.
In our first example we started an event loop and ran our function inside asyncio.run()
That will run your function until all synchronous and non-synchronous calls are complete. I like to think of this step as an instantiation of our asynchronous paradigm.
The real magic — await
Let’s add a new async function and rename some_func
to run
since that is all we are going to have it do -- run our async logic.
import asyncio
async def speak():
print("Hey!")
async def run():
await speak()
asyncio.run(run())
await
tells our event loop to pause here and wait for the coroutine to finish work before continuing on.
If you do not await
the function, then the function will run in series, and the event loop will not yield time to another coroutine. If your function is a coroutine (starts with async def
) then not calling await
on your function will give you an runtime warning:
coroutine 'speak' was never awaited
Starting a function and getting back to it later.
The first thing you might need an async pattern for is starting a job in the background, do other tasks in the front and then coming back to your original task.
import asyncio
async def my_async_func():
print("B")
await asyncio.sleep(1)
print("C")
async def run():
async_func = my_async_func()
print("A")
await async_func
asyncio.run(run())
If you run this you will see the results:
A
B
C
That’s what we wanted right? Close. We wanted to start our async job my_async_func()
and when it was done, return the results. However, the first thing we printed to our terminal was A
not C
. We did not actually start our job, we just defined it. We started it’s execution later when we called await my_aysnc_func
. To start the job running while we move on to other tasks, we need to use asyncio.create_task()
.
import asyncio
async def my_async_func():
print("B")
await asyncio.sleep(1)
print("C")
async def run():
async_func = asyncio.create_task(my_async_func())
await asyncio.sleep(0)
print("A")
await async_func
asyncio.run(run())
Note: I added a 0 second sleep immediately after we did create_task()
. In order for our async_func
task to run, we must yield the event loop, allowing the event loop to schedule the task to begin work.
- We make our coroutine a task
- we call asyncio.sleep(0) to pause execution on
run()
and hop over tomy_async_func
- inside
my_async_func
we print “B”. - The function will continue to do any other work until we hit the next
await
statement where it yields back torun()
. - In run it then prints “A”.
- It then hits
await async_func
where it yields back tomy_async_func
to print “C”
This should result in the following output:
B
A
C
Order mucking
import asyncio
async def my_async_func(letter):
print(letter)
async def run():
# We assign our coroutines to variables
a = my_async_func("a")
b = my_async_func("b")
c = my_async_func("c")
d = my_async_func("d")
# Then we can execute then in any order we wish
await c
await b
await d
await a
asyncio.run(run())
This will return:
c
b
d
a
In the above example, I am showing that you can define your work in any order. Then await them out of order. Or in order. Or out of order and then back in order. Or any combination. Great. They all run sequentially though in the order they are awaited.
Concurrent runnings
import asyncio
async def my_async_func(i):
print("Starting:", i)
await asyncio.sleep(1)
print("Ending:", i)
async def run():
tasks = []
for i in range(3):
tasks.append(asyncio.create_task(my_async_func(i)))
await asyncio.wait(tasks)
asyncio.run(run())
Returns:
Starting: 0
Starting: 1
Starting: 2
Ending: 0
Ending: 1
Ending: 2
Here we are creating a list and then adding all of our items to said list. Then we use wait
to execute our tasks concurrently. Don't forget to await
the asyncio.wait()
. The asyncio.wait
function requires your coroutines to be wrapped in a task
.
What if we want to do something with the results of the functions?
Handling async function results
Our previous example handles the concurrency beautifully. But what if you want to do something with the results of those functions, a la “scatter-gather”? Use gather()
!
import asyncio
async def my_async_func(i):
print("Starting:", i)
await asyncio.sleep(1)
print("Ending:", i)
return (i + 1) * 2 # the first i is a 0, which is why we add 1
async def run():
tasks = []
for i in range(3):
tasks.append(asyncio.create_task(my_async_func(i)))
r = await asyncio.gather(*tasks)
print("Do something with the results:", sum(r)) # sum is just an example of doing something with the results.
asyncio.run(run())
This returns:
Starting: 0
Starting: 1
Starting: 2
Ending: 0
Ending: 1
Ending: 2
Do something with the results: 12
Update (03–31–18): Blocking the loop
One of the most frustrating things you will run into is blocking the event loop. This happens when using a non async function or library. For example if you use requests
(non-async as of this update) it will block the event loop, halting all execution until the http call completes and produces a result. Other things that might block the event loop: time.sleep()
If you need a non-blocking sleep use asyncio.sleep()
Http requests, use a library like aiohttp
for async requesting. AMQP, aioamqp
is a great alternative to Pika. Last example, Databases. Postgres users I recommend asyncpg
and asyncpgsa
.
Example of blocking the event loop.
import asyncio
import time
async def bark():
await asyncio.sleep(3)
time.sleep(3) # This here is the culprit
async def speak():
await bark()
return True
async def run():
r = [speak() for x in range(10)]
await asyncio.wait(r)
loop = asyncio.get_event_loop()
loop.run_until_complete(run())
If we were to profile the above example in Pycharm, you can see that the first 3 seconds all jobs run at the same. Then we hit a blocking call, and each task is forced to wait until the previous jobs sleep finishes.
Update (12–04–18): Unblock the Blockers
I ran into a situation recently where I was using a library that was synchronous. This threw a wrench into how I had designed my worker service. Now, instead of being able to run many jobs simultaneously, all jobs stopped to wait for my synchronous code to run.
loop.run_in_executor()
allows you to take a non async function and put it into an executor, without getting technical makes a sync function async. Take the following code for example:
import asyncio
import time
from concurrent.futures import ThreadPoolExecutor
# This is the blocking function you want to run
def blocking_function():
print("Start blocking")
time.sleep(5) # This is the blocking operation
print("End blocking")
# An async wrapper to run the blocking function in a separate thread
async def run_blocking_function():
loop = asyncio.get_running_loop()
# Run in a default ThreadPoolExecutor
await loop.run_in_executor(None, blocking_function)
# Main function to run async tasks
async def main():
# Running the blocking function without blocking the event loop
await run_blocking_function()
# Run the main function
asyncio.run(main())
In this example, loop.run_in_executor(None, blocking_function)
runs the blocking function in a separate thread. By passing None
as the first argument, it uses the default ThreadPoolExecutor
. This approach allows the rest of your asyncio program to continue running asynchronously, even while waiting for the blocking operation to complete.
Skyler Lewis makes no claims to expertise or exactness of the information presented and may not reflect the opinions of the owners or advertisers. Parental guidance isn’t recommended. Any rebroadcast, retransmission, or account of this article, without the express written consent of Major League Baseball, is not prohibited. Use as directed. May cause sleepiness, headaches, backaches, or thoughts of bacon wrapped shrimp.