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