I have been learning and exploring Python asyncio
for a while. Before starting this journey I have read loads of articles to understand the subtle differences between multithreading
, multiprocessing
, and asyncio
. But, as far as I know, I missed something on about a fundamental issue. I'll try to explain what I mean by pseudocodes below.
import asyncio
import time
async def io_bound():
print("Running io_bound...")
await asyncio.sleep(3)
async def main():
start = time.perf_counter()
result_1 = await io_bound()
result_2 = await io_bound()
end = time.perf_counter()
print(f"Finished in {round(end - start, 0)} second(s).")
asyncio.run(main())
For sure, it will take around 6 seconds because we called the io_bound
coroutine directly twice and didn't put them to the event loop. This also means that they were not run concurrently. If I would like to run them concurrently I will have to use asyncio.gather(*tasks)
feature. I run them concurrently it would only take 3 seconds for sure.
Let's imagine this io_bound
coroutine is a coroutine that queries a database to get back some data. This application could be built with FastAPI roughly as follows.
from fastapi import FastAPI
app = FastAPI()
@app.get("/async-example")
async def async_example():
result_1 = await get_user()
result_2 = await get_countries()
if result_1:
return {"result": result_2}
return {"result": None}
Let's say the get_user
and get_countries
methods take 3 seconds each and have asynchronous queries implemented correctly. My questions are:
asyncio.gather(*tasks)
for these two database queries? If necessary, why? If not, why?io_bound
, which I call twice, and get_user
and get_countries
, which I call back to back, in the above example?io_bound
example, if I did the same thing in FastAPI, wouldn't it take only 6 seconds to give a response back? If so, why not 3 seconds?asyncio.gather(*tasks)
in an endpoint?Do you need to? Nope, what you have done works. The request will take 6 seconds but will not be blocking so if you had another request coming in, FastAPI can process the two requests at the same time. I.e. two requests coming in at the same time will take 6 seconds still, rather than 12 seconds.
If the two functions get_user() and get_countries() are independant of eachother, then you can get the run the functions concurrently using either asyncio.gather
or any of the many other ways of doing it in asyncio, which will mean the request will now take just 3 seconds. For example:
async def main():
start = time.perf_counter()
result_1_task = asyncio.create_task(io_bound())
result_2_task = asyncio.create_task(io_bound())
result_1 = await result_1_task
result_2 = await result_2_task
end = time.perf_counter()
print(f"Finished in {round(end - start, 0)} second(s).")
or
async def main_2():
start = time.perf_counter()
results = await asyncio.gather(io_bound(), io_bound())
end = time.perf_counter()
print(f"Finished in {round(end - start, 0)} second(s).")
assuming get_user and get_countries just call io_bound, nothing.
It will take 6 seconds. FastAPI doesn't do magic to change the way your functions work, it just allows you to create a server that can easily run asynchronous functions.
When you want run two or more asyncronous functions concurrently. This is the same, regardless of if you are using FastAPI or any other asynchronous code in python.