Асинхронность в FastAPI / Python

Как сделать, чтобы при получении, например, двух запросов в FastAPI, они выполнялись независимо друг от друга. То есть, если первым придет запрос на обработку, который занимает 20 сек, а вторым придет запрос, который занимает 5 сек, то результат второго запроса возвращался раньше первого? В данный момент имеется такой пример (в async функциях yield/thread/multiproccessing не присутствуют):

if __name__ == "__main__":
    uvicorn.run("main:app", host= host, port= port, log_level="info", loop="asyncio")


@app.post("/bar_logo")
async def add_bar_logo(pos: str = Form(""), fio: str = Form(""), phone: str = Form(""), date: str = 
Form(""), file: UploadFile = File(...)):
    file_data = file.file.read()
    res_pdf = await bar_logo(file_data, pos, fio, phone, date)
    return FileResponse(res_pdf, media_type="application/pdf", filename=file.filename)


async def bar_logo(file_data, pos, fio, phone, date):
    uuid = uuid4()
    doc = fitz.open(stream=file_data, filetype="pdf")
    if doc[0].rect.width > doc[0].rect.height:
        l_path = await draw_bar_w(pos, fio, phone, date)
        with open(l_path, "rb") as f:
            logo = fitz.Pixmap(f.read())
    else:
        l_path = await draw_bar_h(pos, fio, phone, date)
        with open(l_path, "rb") as f:
            logo = fitz.Pixmap(f.read())
    doc[-1].insert_image(doc[-1].rect, pixmap=logo)
    save_dir = f"../results/{uuid}.pdf"
    doc.save(save_dir, garbage=2, deflate=True, encryption=encrypt_meth, permissions=perm, owner_pw="")
    return save_dir

Ответы (2 шт):

Автор решения: Andrew Holovko

Базовый пример, выполняется асинхронно.

import asyncio

import uvicorn as uvicorn
from fastapi import FastAPI

app = FastAPI()


@app.get("/one")
async def read_root():
    await asyncio.sleep(10)
    return {"first_root": "one"}


@app.get("/two")
async def read_item():
    return {"second_root": "two"}

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

https://fastapi.tiangolo.com/async/

https://fastapi.tiangolo.com/#create-it

→ Ссылка
Автор решения: Влад Виноградов

Есть асинхронные фреймворки, есть синхронные. В целом идея такая: асинхронные по умолчанию выполняют все запросы в 1 потоке, синхронные выполняют каждый запрос в отдельном потоке. Если писать на async, то никаких доп задержек быть не должно, как cpu bound так и io bound без async.

async def - не значит, что функция выполняется асинхронно.

Посмотрев на ваш пример, можно увидеть что async def bar_logo - синхронно читает из файловой системы => io задержка, которая блочит весь event loop. Блокировка event loop = все клиенты ждут.

Вот и получается, что 1 запрос прилетел и так как там блочится event loop, все остальные запросы не могут быть обработаны. Это очень грубо, но в общем работает это примерно так.


Чтобы этого избежать нужно использовать специальные асинхронные либы, aiofiles например для чтения файлов.

А вот если либы специальной нет, то все io вещи нужно заворачивать в потоки. Можно присмотреться к anyio библиотеки, которая облегчит тут жизнь.

from anyio import to_thread


async def io_bound(io_fun, params_fun):
    await to_thread.run_sync(io_fun, params_fun)

Потоки - хорошо подходят для io bound задач и плохо подходят для cpu bound задач, где требуются вычислительные ресурсы. Это связано с существованием GIL.
Поэтому для cpu bound задач можно использовать процессы.

Вот очень простой пример:

async def cpu_bound(fun_cpu_bound, params_fun):
    with concurrent.futures.ProcessPoolExecutor() as executor:
        return await loop.run_in_executor(executor, fun_cpu_bound, params_fun)
→ Ссылка