当サイトは、アフィリエイト広告を利用しています
PythonのFastAPIを使ってREST APIを実装していると、次のようなことが疑問を持った。
FastAPIは「非同期対応フレームワーク」と言われるが
しかし実際にどの単位で、どのレイヤーで並行処理が行われているのかを構造的に
理解していると気づいたので、調べてみた。
本記事では、FastAPIの非同期、並行処理の構造を以下の順で整理する。
非同期処理とは、
-待ち時間で処理をブロックしない設計
のこと。
I/O処理の完了を待つ間にスレッドを占有しないようにすること。
ここで重要なのは、
つまり、整理すると
になる。
並行処理を実現するためには、処理を一時停止し、制御を他へ渡せる仕組みが必要になる。
その役割を担うのが非同期処理である。
並行処理(Concurrency)とは
だが、実際には1つのスレッドが処理を切り替えながら進めている
並列(Parallelism)とは異なるので注意が必要。
整理すると
になる。図にすると下記のようなイメージ
並列:AAAAABBBBB並行:AABBABABBA
FastAPIのasyncは並列ではなく、並行処理の構造になる
前提の基本的な概念として
がある。
ではここからはFastAPIにおいては、どんな構造で並行処理を実現させているのかを
整理する
FastAPIは単独で並行処理を実装しているわけではない。
並行処理を成立させているのは
である。
FastAPIはこれらの上に乗って動作するアプリケーションである。
FastAPIは Pythonのasyncio上に構築されたWebフレームワークなので
asyncioやコルーチンついて先に整理する。
asyncioは
で
を行う
FastAPIのasyncは実際にはasyncio上で動作している。
async def で定義されたものはコルーチン関数となる
async def read():return 1
呼び出すと
coro = read()print(type(coro))# <class 'coroutine'>
返るのはまだ実行されていないオブジェクト。
これは「実行途中で停止可能な処理のインスタンス」であり、
まだ処理は開始されていない。
実行するには
result = await coroprint(result)# 1
await を付けることで
されるようになる。
asyncio└── Event Loop├── Task(Coroutine A)├── Task(Coroutine B)└── Task(Coroutine C)
イベントループが直接管理するのはTaskであり、Taskの中でCoroutineが実行される。
┌─────────────────────┐│ asyncio ││ ││ ┌─────────────┐ ││ │ Event Loop │ ││ │ │ ││ │ A → await │ ││ │ B → 実行 │ ││ │ C → 待機 │ ││ └─────────────┘ │└─────────────────────┘
動作の流れ
関係を整理すると
タスクをイベントループが管理し、その全体をasyncioが提供している。
asyncioについては下記記事でも解説してます
全体構造はこうなる。
クライアント↓Uvicorn(ASGIサーバ)↓asyncioイベントループ↓FastAPI(ASGIアプリ)↓エンドポイント(コルーチン)
関係する技術としては
がある
ASGIとは:
サーバとアプリの通信契約を定義している。
Uvicornは
で役割としては
がある
FastAPIアプリをUvicorn上で起動した場合、Uvicornから呼び出される。
Uvicornは
をしている。
実装例でいうと
from app.main import create_appapp = create_app()
FastApiアプリケーションをASGIサーバーで起動するための
エントリーポイントとなるモジュール
ASGIサーバーは起動時、ここにFastApiアプリケーションインスタンスを探しにくる
from fastapi import FastAPIimport app.endpoints as endpointsdef create_app():# アプリケーションインスタンスの作成app = FastAPI()# ルーティング設定app.include_router(endpoints.router)return app
FastAPIは Pythonのasyncio上に構築されたWebフレームワークなので
を行う
1プロセス└── asyncio└── Event Loop├── Task(Request A コルーチン)├── Task(Request B コルーチン)└── Task(Request C コルーチン)
コルーチン内でI/O待ちでawaitすると、
ので、並行処理の単位は「リクエスト単位のタスク」となる
※リクエストA,B,Cが並行実行される
@app.get("/")async def read():return {"ok": True}
のようなエンドポイント関数があった場合
FastAPIは
つまり、
FastAPIのエンドポイントはコルーチン関数であり、
リクエストごとに生成されるコルーチンオブジェクトが並行処理の実行単位になる。
Pythonのコルーチンはawaitつけないと実行されないので
このエンドポイント関数は実はFastAPI自身というより、ASGIサーバ側が内部的にawaitし実行している。
FastAPI は アプリケーションなので、OSプロセスを管理する責務は持たない。
プロセス管理を行うのは:
つまり
FastAPIはその中で動く「ASGIアプリ」でしかない
1プロセス└── asyncio└── Event Loop├── Task(Request A コルーチン)├── Task(Request B コルーチン)└── Task(Request C コルーチン)2プロセス└── asyncio└── Event Loop├── Task(Request A コルーチン)├── Task(Request B コルーチン)└── Task(Request C コルーチン)
※高度なカスタム構成を除けば、基本は「1プロセス1イベントループ」。
つまり、並行処理は「1プロセス1イベントループ」内で行われる。
上の例だと1プロセス数と2プロセスは別の並行処理になる
uvicornであれば、起動時のコマンドでworker数を指定すればプロセス数を指定できる
uvicorn app:app --workers 4
この場合
整理すると
Uvicornの起動については下記記事でも解説してます
最後に誤解しやすいポイントをまとめてく
FastAPIの並行処理はASGI仕様に基づき
ことで成立している。
実際にFastAPIを使ったREST APIの実装例に関しては下記記事で紹介しています