AsyncIO¶
Python’s asyncio library allows you to run single threaded “concurrent” code using coroutines inside an event loop.
The event loop is designed for I/O over sockets and other resources, it is especially good for working with client/server network connections.
Python >= 3.4 (best features and performance in 3.6).
This guide will assume you’re on Python 3.7.
Explanation¶
Benefits¶
The event loop allows you to handle a larger number of network connections at once.
Non-blocking network IO, so you can have long running connections with very little performance impact (HTML5 sockets for example).
How web servers are typically designed¶
(Pyramid, Flask, Plone, etc)
- Processes X Threads = Total number of concurrent connections that can be handled at once.
- Client makes a request to web server, request is assigned thread, thread handle request and sends response
- If no threads available, request is blocked, waiting for an open thread
- Threads are expensive (CPU), Processes are expensive on RAM
How it works with AsyncIO¶
- All requests are thrown on thread loop
- Since we don’t block on network traffic, we can juggle many requests at the same time
- Modern web application servers connect with many different services that can potentially block on network traffic — BAD
- Limiting factor is maxed out CPU, not costly thread switching between requests — GOOD
Where is network traffic used?¶
- Web Client/App Server
- App Server/Database
- App Server/Caching(Redis)
- App Server/OAUTH
- App Server/Cloud storage
- App Server/APIs(Google Drive, Microsoft, Slack, etc)
Implementation details¶
To benefit, the whole stack needs to be asyncio-aware.
Anywhere in your application server that is not and does network traffic
WILL BLOCK all other connections while it is doing its network traffic
(example: using the requests
library instead of aiohttp
)
Basics¶
Get active event loop or create new one
Run coroutine inside event loop with asyncio.run
import asyncio
async def hello():
print('hi')
if __name__ == '__main__':
asyncio.run(hello())
Basics(2)¶
asyncio.run
automatically wraps your coroutine into a Future
object and waits for it to finish.
asyncio.create_task
will wrap a coroutine in a future and return it to you.
To demonstrate this you can schedule multiple coroutines that can run at the same time.
import asyncio
async def hello1():
print('before hi 1')
await asyncio.sleep(0.5)
print('hi 1')
async def hello2():
print('hi 2')
async def run():
future1 = asyncio.create_task(hello1())
future2 = asyncio.create_task(hello2())
await future1
await future2
if __name__ == '__main__':
asyncio.run(run())
Long running tasks¶
You can also schedule long running tasks on the event loop.
The tasks can run forever…
“Task” objects are the same as “Future” objects (well, close)
import asyncio
import random
async def hello_many():
while True:
number = random.randint(0, 3)
await asyncio.sleep(number)
print('Hello {}'.format(number))
async def run():
task = asyncio.create_task(hello_many())
print('task running now...')
await asyncio.sleep(10)
print('we waited 10 seconds')
task.cancel()
print('task cancelled')
if __name__ == '__main__':
asyncio.run(run())
ALL YOUR ASYNC BELONGS TO US¶
gotcha
If you want part of your code to be async (say a function), the complete stack of the caller must be async and running on the event loop.
import asyncio
async def print_foobar1():
print('foobar1')
async def print_foobar2():
print('foobar2')
async def foobar():
await print_foobar1()
print_foobar2() # won't work, never awaited
async def run():
await foobar()
print_foobar1() # won't work, never awaited
if __name__ == '__main__':
asyncio.run(run())
# await print_foobar1() # error, not running in event loop
“multi” processing¶
AsyncIO isn’t really multiprocessing but it gives you the illusion of it.
A simple example can be shown with the asyncio.gather
function.
import asyncio
import aiohttp
async def download_url(url):
async with aiohttp.ClientSession() as session:
resp = await session.get(url)
text = await resp.text()
print(f'Downloaded {url}, size {len(text)}')
async def run():
await asyncio.gather(
download_url('https://www.google.com'),
download_url('https://www.facebook.com'),
download_url('https://www.twitter.com'),
download_url('https://www.stackoverflow.com')
)
if __name__ == '__main__':
asyncio.run(run())
AsyncIO loops¶
Using yield
with loops allows you to “give up” execution on every iteration of the loop.
import asyncio
async def yielding():
for idx in range(5):
print(f'Before yield {idx}')
yield idx
async def run():
async for idx in yielding():
print(f"Yay, I've been yield'd {idx}")
if __name__ == '__main__':
asyncio.run(run())
Scheduling¶
loop.call_later
: arrange to call on a delay
loop.call_at
: arrange function to be called at specified time
Executors¶
An executor is available to use when you have non-async code that needs to be made async.
A typical executor is a thread executor. This means, anything you run in an executor is being thrown in a thread to run.
It’s worse to have non-async code than to use thread executors.
Executors are also good for CPU bound code.
import asyncio
import requests
import concurrent.futures
def download_url(url):
resp = requests.get(url)
text = resp.content
print(f'Downloaded {url}, size {len(text)}')
async def foobar():
print('foobar')
async def run():
executor = concurrent.futures.ThreadPoolExecutor(max_workers=5)
event_loop = asyncio.get_event_loop()
await asyncio.gather(
event_loop.run_in_executor(executor, download_url, 'https://www.google.com'),
event_loop.run_in_executor(executor, download_url, 'https://www.facebook.com'),
event_loop.run_in_executor(executor, download_url, 'https://www.twitter.com'),
event_loop.run_in_executor(executor, download_url, 'https://www.stackoverflow.com'),
foobar()
)
if __name__ == '__main__':
asyncio.run(run())
Subprocess¶
Python also provides a very neat AsyncIO subprocess module.
import asyncio
async def run_cmd(cmd):
print(f'Executing: {" ".join(cmd)}')
process = await asyncio.create_subprocess_exec(*cmd, stdout=asyncio.subprocess.PIPE)
out, error = await process.communicate()
print(out.decode('utf8'))
async def run():
await asyncio.gather(
run_cmd(['sleep', '1']),
run_cmd(['echo', 'hello'])
)
if __name__ == '__main__':
asyncio.run(run())