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:
- Do I need to use
asyncio.gather(*tasks)for these two database queries? If necessary, why? If not, why? - What is the difference between
io_bound, which I call twice, andget_userandget_countries, which I call back to back, in the above example? - In the
io_boundexample, 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? - In the context of FastAPI, when would be the right time to use
asyncio.gather(*tasks)in an endpoint?