A recent article by Jason Goldstein expressed the author’s difficulty understanding and using Asyncio, especially in a Flask context. Asyncio in a Flask context is the exact experience I have with Quart, so I hope I can add something to the conversation this author started.
Asyncio provides another tool for concurrent programming in Python, that is more lightweight than threads or multiprocessing. In a very simple sense it does this by having an event loop execute a collection of tasks, with a key difference being that each task chooses when to yield control back to the event loop.
With Asyncio the yield must be explicitly coded, by using the
await keyword to await something that is asynchronous from within something that is asynchronous. Additionally anything that is asynchronous must also be explicitly marked as such, using the
async keyword. This causes an immediate headache as most existing code does not do this, and hence Asyncio does not work with this code. It then causes a second headache in that now everything bar the main function must be asynchronous if anything is asynchronous.
The reason to switch to Asyncio accepting the additional complication is hinted in its name, IO operations. Synchronous IO operations block execution until the IO is complete whereas asynchronous IO operations yield to the event loop allowing something else to be executed.
Feedparser and Sans-IO
The Feedparser library that Goldstein wished to use has no asynchronous code, so Goldstein wrapped the synchronous calls in asynchronous functions. This allowed Asyncio to be used, but offers only complication as the IO operations still blocked execution.
Feedparser like many Python libraries tries to be as helpful as it can be by combining the IO and parsing. Yet with the advent of Asyncio, libraries can be more helpful by being Sans-IO, i.e. leaving the IO as a choice of the library user. As it turns out Feedparser can do this,
async def fetch_feed(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
raw_feed = await response.text()
The key, to reiterate, is that the response (text) is awaited yielding this execution to the event loop thereby allowing other requests to be made concurrently. The event loop returns to this specific execution when the relevant IO operation has completed.
Yielding to the event loop
The yield in the above example, and in general for Asyncio is explicit and therefore clear to reason about, equally it is hopefully now clear that something as simple as
async def hello_world():
does not yield, and will not run concurrently. What is less clear is that this example
async def outer():
async def inner():
will also not yield, and hence not run concurrently. I think the pre Python3.5 syntax makes this clear
yield from inner()
as there is clearly no
yield. A good rule of thumb is that only the Asyncio primitives actually yield, and the established practice is to
await asyncio.sleep(0) if you want to ensure a yield.
Goldstein’s also questioned why
async with session.get(url) as response: is required to make a request with Aiohttp. Specifically I think he is asking why
async with rather than just a pain
with. As is the common theme of this article, Python is explicit regarding asynchronous calls, and hence if the try, finally combination the
with replaces has an
await it must be an
async with. For example if the session must be opened or finalised via an await,
response = await session.get(url)
with cannot be used.
Creating the event loop
The final aspect of Goldstein’s article relates to creating the event loop itself within a Flask view function. As Flask does a lot of IO (every request/response at least), I think Flask should handle the event loop, which is what Quart does. So instead of the Flask example he gave, I propose a Quart example