https://christina04.hatenablog.com/entry/fastapi-def-vs-async-def
FastAPIでPath Operationに def と async def どちらを使うべきか
背景
FastAPIでは以下のようにデコレータ関数を使うことでHTTPサーバのpathを設定することができ、これをPath Operationと呼びます。
このPath Operationでdef
を使うべきかasync def
を使うべきかの方針を説明します。
環境
- Python 3.9.11
- FastAPI 0.74.1
方針
結論から言うと以下の方針で実装しましょう。
await
があるならasync def
を使う- それ以外は
def
を使う
理由
理由は以下になります。
- CPUバウンドな処理だったら
async def
もdef
も大きく変わらない - 同期処理でIOバウンドの場合は外部スレッドプールを使う
def
が優れている await
がある非同期処理はasync def
が必須
1つ1つ説明していきます。
FastAPIはメインasync loopと外部スレッドプールがある
まず前提知識としてFastAPIにはメインasync loopと外部スレッドプールがあり、
def
→外部スレッドプールasync def
→メインasync loop
で実行されます。
外部スレッドプールはマルチスレッドに見えるが実質シングルスレッド
上図では外部スレッドプールはマルチスレッドのように見えます。
しかしPythonではGIL(グローバルインタプリタロック)を使っており、これはスレッドセーフでないコード(C言語ライブラリなど)を他のスレッドと共有してしまうことを防ぐための排他ロックであるため、同時に実行できるスレッドは1つに制限されます。
なのでCPUバウンドな処理では外部スレッドプールであろうと、メインasync loopのスレッドであろうと違いはありません。
外部スレッドプールでは同期IO中に他のスレッドに切り替えできる
しかしIOバウンドな同期処理の場合は、CPUは使わず単に待ち時間でブロックされるため、複数のスレッドを持っている方が有利です。
メインasync loopでは処理がブロックされるため3つしか処理が実行できないのに対して、複数のスレッドがある外部スレッドプールでは待つ間にスレッドを切り替えて多くのリクエストを処理できます。
await
がある非同期処理ではasync def
await
がある非同期処理はそもそもasync def
内でしか使えません。
You can only use await inside of functions created with async def.
ref: Concurrency and async / await - FastAPI
同期・非同期の区別については以下の過去記事が参考になると思います。
先程ブロックされていたメインasync loopは非同期IOなのでブロックされず、マルチスレッドの同期IOのように準備中は次のイベントに、準備が完了したらそのイベントの処理を完了する形になります。
ベンチマーク
以下は実際のベンチマーク結果です。左から
- 同期処理を
def
で実行 - 非同期処理を
async def
で実行 - 同期処理を
async def
で実行
となっています。
IOバウンドな同期処理をasync def
内で実行すると前述のようにブロックされてパフォーマンスが低いことが分かります。
一方以下のベンチマークではCPUバウンドな処理はasync def
の方がパフォーマンスが良かったとあります。
スレッドの切り替えはコンテキストスイッチが発生するので、その分パフォーマンスに影響したのではと思われます。
かといってどの関数がCPUバウンドか、を常に意識するのは実装やレビューでコストになるため、基本的には同程度のパフォーマンスと考えてdef
優先にするのが良いです。
まとめ
- CPUバウンドな処理だったらasync defもdefも大きく変わらない
- 同期処理でIOバウンドの場合は外部スレッドプールを使うdefが優れている
- awaitがある非同期処理はasync defが必須
といったことから
- awaitがあるならasync defを使う
- それ以外はdefを使う
と判断すれば良いでしょう。
0 コメント:
コメントを投稿