FastAPIの非同期処理と並行処理の使い分け
業務でFastAPIのパフォーマンスチューニングに触れる機会があり、非同期処理(async def)と並行処理(def)の違いってなんだっけ?みたいな感じになったので、備忘録で残します。
はじめに(おさらい)
1. 同期処理とは?
同期処理は、タスクを一つずつ順番に処理する方法です。一つのタスクが終わるまで次のタスクは待機します。
# 同期処理の例
def make_banana_smoothie():
peel_banana() # 完了するまで待つ
cut_banana() # 完了するまで待つ
blend_banana() # 完了するまで待つ
return "スムージー完成!"
同期処理のメリット・デメリット
メリット:
- シンプルで分かりやすい
- デバッグが容易
デメリット:
- I/O(アイオー) 待ち時間が無駄になる
- 処理時間が長くなりがち
2. 並列処理とは?
並列処理は、複数のタスクを文字通り同時に実行する方法です。複数のCPUコアを使って、異なるタスクを同時に処理します。
# multiprocessingを使った並列処理
import multiprocessing
def process_data_chunk(chunk):
# 計算集約型の処理
result = heavy_calculation(chunk)
return result
if __name__ == '__main__':
# 大きなデータを分割
data_chunks = split_data(big_data, 4)
# プロセスプールを作成(CPUコア数に合わせる)
with multiprocessing.Pool(processes=4) as pool:
# 複数プロセスで同時に計算
results = pool.map(process_data_chunk, data_chunks)
# 結果を結合
final_result = combine_results(results)
並列処理のメリット・デメリット
メリット:
- 複数のCPUコアを同時に活用できる
- プロセス間は独立しているため、単一のプロセスが失敗しても全体に影響しにくい
デメリット:
- プロセス間通信のオーバーヘッドがある
- メモリ使用量が増加する
- データ共有がしづらい(しないほうがいい)
- 各プロセスは独立したメモリ空間を持つため、データを共有するには仕組みが必要
3. 並行処理とは?
並行処理は、複数のタスクを切り替えながら処理する方法です。Python では主にスレッドや子プロセスを使います。
# スレッドを使った並行処理
import threading
def process_bananas():
thread1 = threading.Thread(target=peel_banana)
thread2 = threading.Thread(target=cut_pineapple)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
並行処理のメリット・デメリット
メリット:
- I/O 待ち時間を有効活用できる
- メモリ空間を共有するため、データ共有が容易
- スレッド切り替えのコストが比較的小さい
デメリット:
- リソース競合が発生する可能性がある
- デバッグがしづらい
4. 非同期処理とは?
非同期処理は、I/O 待ち時間に別の作業を行う方法です。Python では asyncio
を使います。
# 非同期処理の例
import asyncio
async def process_fruit():
task1 = asyncio.create_task(async_peel_banana())
task2 = asyncio.create_task(async_cut_pineapple())
await task1
await task2
async def main():
await process_fruit()
asyncio.run(main())
非同期処理のメリット・デメリット
メリット:
- I/O 待ち時間を最大限に活用
- 単一スレッドでリソース競合を回避
デメリット:
- すべてのライブラリが非同期対応しているわけではない
5. 料理でのイメージ
同期処理
一人の料理人が一つの料理を最初から最後まで順番に作業。次の工程には前の工程が完了後にしか進めない。(シングルスレッド)
並列処理
複数の料理人が別々の料理を同時に完全独立して調理。お互いの作業に影響なし。(マルチプロセス)
並行処理
複数の料理人による流れ作業。材料係→調理係→盛付け係と分担し連携。全員が同時に別の工程で作業していますが、連携して一つの料理を作り上げる。(マルチスレッド)
非同期処理
一人の料理人が効率的に作業。肉を焼いている間に野菜を切るなど、待ち時間を活用して複数のタスクを切り替えながら進める。(イベントループ)
(本題)FastAPIにおける非同期処理と並行処理の使い分け
結論
-
async def
→ 非同期処理 -
def
→ 並行処理(マルチスレッド) - 基本的には
def
を推奨します
処理の仕組み
FastAPIは内部的にマルチスレッドを使って実行するため、複数のリクエストが同時に来ても、マルチスレッドで処理可能な数までは並行して処理できます。
一方、非同期処理はシングルスレッドで動作します。このため、await
が付いていない処理を実行している間は、他のリクエストの処理がブロックされてしまいます。使用するライブラリやメソッドがawait対応してるのが必須です。
公式の推奨事項
・await を使って呼び出すサードパーティライブラリを使用している場合: path operation関数は async def で宣言してください。
・データベース、API、ファイルシステムなどと通信し、awaitをサポートしていないサードパーティライブラリ(現在のほとんどのデータベースライブラリが該当)を使用している場合: 普通に def を使ってpath operation関数を宣言してください。
・アプリケーションが他の何とも通信せず、応答を待つ必要がない場合: async def を使ってください。
・よく分からない場合は、通常の def を使ってください。
引用元: https://fastapi.tiangolo.com/ja/async/
サンプル
OpenAIの画像分析結果を出力して保存するケース
非同期クライアント(AsyncOpenAI)がawaitをサポートしているため、async def
を使用しています。これによりOpenAIからの応答を待つ間に他の処理を行うことができ、出力を早くすることができます。
from fastapi import FastAPI
from openai import AsyncOpenAI
import os
app = FastAPI()
client = AsyncOpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
@app.post("/analyze-image")
async def analyze_image(image_url: str):
# OpenAIの非同期クライアントを使用して画像分析
response = await client.chat.completions.create(
model="gpt-4-vision-preview",
messages=[
{
"role": "user",
"content": [
{"type": "text", "text": "この画像を分析してください"},
{"type": "image_url", "image_url": {"url": image_url}}
]
}
],
max_tokens=300
)
analysis_result = response.choices[0].message.content
# 結果を返す
return {"analysis": analysis_result}
まとめ
無闇に時間のかかりうる処理を非同期処理にするのではなく、使用するライブラリが非同期(async)対応しているか確認した方がいい。非同期対応しているならasync def
で定義し、対応していない場合はdef
で定義する方がベターです
Discussion