async defとは、fastapiにおける頻出表記ですね。いつもちゃんと理解ができなくて、あやふややってきました。今年2月時点で、fastapi公式ドキュメントの並行処理とasync/awaitがようやく日本語化され、それを読んでから記事を作成しました。(訳者さんに感謝〜)
結論
APIサーバーの動き
fastapiは、クライアントサーバ(client-server, cs)システムの一つです。サーバー上では、fastapiアプリはASGIサーバー(uvicornが一般的)から起動されます。uvicorn起動オプションの一つ--workers
でスレッド数が指定されます。デフォルトでは環境変数WEB_CONCURRENCYを参照し、ない場合は1とします。
多重化(multiplexing)技術を使って、複数のTCP接続を一つのスレッドで管理します。
async defとdefの違い
- Path operation関数asyncで宣言すると、fastapiのメインスレッドで直接実行されます。awaitされるので、メインスレッドをブロックします。時間消費が多い場合、サーバーが新しいリクエストへ返答が遅くなる可能性があります。
- 一方で、defで宣言すると、メインスレッドじゃなくて外部スレッドプールで実行されます。メインスレッドをブロックしないが、cpuに負荷をかけます。
結局、どっちを使うのか
答えは公式ドキュメントにあります。
path operation 関数を async def の代わりに通常の def で宣言すると、(サーバーをブロックするので) 直接呼び出す代わりに外部スレッドプール (awaitされる) で実行されます。
上記の方法と違った方法の別の非同期フレームワークから来ており、小さなパフォーマンス向上 (約100ナノ秒) のために通常の def を使用して些細な演算のみ行う path operation 関数 を定義するのに慣れている場合は、FastAPIではまったく逆の効果になることに注意してください。このような場合、path operation 関数 がブロッキングI/Oを実行しないのであれば、async def の使用をお勧めします。
参照先:https://fastapi.tiangolo.com/ja/async/#path-operation
コードで検証
"async def"と"def"違いを検証するために、demoを用意しました。
import threading
from fastapi import FastAPI
from time import sleep
from hashlib import md5
from random import randint
app = FastAPI()
def work(n):
sleep(n)
b = randint(602, 2021).to_bytes(4, 'big')
return md5(b).hexdigest()
async def awork(n):
sleep(n)
b = randint(602, 2021).to_bytes(4, 'big')
return md5(b).hexdigest()
@app.get('/awork')
async def awk(n: int = 7):
res = await awork(n)
print(123)
return res
@app.get('/work')
def wk(n: int = 7):
res = work(n)
return res
@app.get('/aping')
async def aping():
return 'APONG!'
@app.get('/ping')
def ping():
return 'PONG!'
@app.get('/tid')
def tid():
return threading.get_ident()
github: https://github.com/tdzz1102/fastapi-async-what
localhost:8000でサーバーを立ち上げて、api docsはこうなっている。
work(), awork()
の中ではブロッキング処理があります。唯一の違いは、awork()
はasyncで宣言されます。それぞれにapiも作成しました。ping(), aping()
の役割は、サーバーが今ブロックされるかの確認。tid()
はthreading.get_ident()
、つまりスレッドidを取得する関数です。
操作 | 結果 | 原因 |
---|---|---|
ブラウザの違うページから/tidを実行 | tidは同じ | fastapiはシングルスレッド |
/workしてからすぐに/pingを実行 | /pingはすぐに結果が返される | 両方とも外部スレッドプール実行されるので、メインスレッドをブロックしない |
/workしてからすぐに/apingを実行 | /apingはすぐに結果が返される | /workは外部スレッドプール実行されるので、メインスレッドをブロックしない |
/aworkしてからすぐに/apingを実行 | /apingは約7秒後に結果が返される | メインスレッドは/aworkを待つ(await)ので、7秒間ブロックされる |
/aworkしてからすぐに/pingを実行 | /pingは約7秒後に結果が返される | メインスレッドは/aworkを待つ(await)ので、7秒間ブロックされる。/pingはスレッドプールが代わりに実行するが、それにリクエストの「受け取り-スレッド作成-結果を待つ」という流れも、メインスレッドの役割だから |