はじめに
- 同期処理 vs 非同期処理
- 並行処理 vs 並列処理
- プロセス vs スレッド
- I/Oバウンド vs CPUバウンド
- イベントループ vs スレッドプール
- GIL (Global Interpreter Lock)
皆さんは,これらの用語の関係性を他人にわかりやすく説明できますか?
「なんとなく async def を使っている」「重い処理は await すればいいんでしょ?」
もしそう思っているなら,あなたのFastAPIアプリケーションは本来の性能を発揮できていないかもしれません.
本記事では,あやふやになりがちなこれらの概念を整理し,FastAPIが裏側でどのようにリクエストを捌いているのかを,図解を交えて解説します.
※ 本記事はFastAPI(Python)を主軸に解説します.
1. 概念の整理:処理の「進め方」と「実行環境」
まず,「同期・非同期」と「並行・並列」という,よく混同される2つの対比軸を明確にします.
1-1. 同期処理 vs 非同期処理
これは「タスクの待ち時間をどう扱うか」という,時間の使い方の話です.カフェの注文を例に考えると直感的です.
同期処理(Sync)
「前の人が終わるまで,列から動けない」状態です.
- レジで注文する
- 店員がコーヒーを淹れ終わるまで,レジの前でじっと待つ(ブロッキング)
- コーヒーを受け取ってから,ようやく次の人が注文できる
非同期処理(Async)
「注文だけして,出来上がるまで別のことをする」状態です.
- レジで注文する
- 番号札(Future/Task)を受け取り,列を離れる(すぐに次の人の注文を受け付ける)
- 席でスマホを見たり(別の処理),他の作業をする
- 呼び出されたら(完了通知),コーヒーを受け取る
| 特徴 | 同期処理 (Sync) | 非同期処理 (Async) |
|---|---|---|
| 実行順序 | コードの記述順通り | 完了順(前後する可能性がある) |
| 待ち時間 | 何もしない(ブロック) | 別のタスクを処理する |
| メリット | 直感的でデバッグしやすい | I/O待ちが多いシステムで高効率 |
| デメリット | 大量のリクエストで詰まる | 制御が複雑になる |
1-2. 並行処理 vs 並列処理
これは「いくつの作業主体(手)で実行するか」という,リソースの使い方の話です.こちらはキッチンのシェフを例にします.
並行処理(Concurrency)
「1人のシェフが,複数の料理を同時進行する」状態です.
- シェフは1人
- 「野菜を切る」→「鍋を火にかける」→「煮込んでいる間に肉を切る」といったように,タスクを細かく切り替えながら作業する
- ある「瞬間」を見れば,やっていることは1つだけ.しかし,素早い切り替え(コンテキストスイッチ)により,人間からは同時に進んでいるように見える
並列処理(Parallelism)
「複数のシェフが,完全に同時に料理する」状態です.
- シェフは複数人(マルチコア)
- シェフAが「野菜を切る」のと全く同じ瞬間に,シェフBが「肉を焼く」
- 物理的に同時に動いている
| 特徴 | 並行処理 (Concurrency) | 並列処理 (Parallelism) |
|---|---|---|
| 作業主体 | 論理的に複数(実体は1つの場合も) | 物理的に複数(マルチコア) |
| 動作原理 | タスクの高速な切り替え | 同時実行 |
| 得意分野 | I/Oバウンド(待ち時間の有効活用) | CPUバウンド(計算速度の向上) |
2. 知っておくべき重要用語
仕組みを理解するための「部品」について定義します.より深く理解したい方は,リンク先の良記事も併せて参照してください.
プロセスとスレッド
プログラムが動くときの「箱(プロセス)」と「作業者(スレッド)」の関係です.
I/OバウンドとCPUバウンド
処理の遅延原因(ボトルネック)が「通信などの待ち時間」にあるか,「計算処理」にあるかの違いです.
GIL (Global Interpreter Lock)
Python(CPython)において,1つのプロセス内では同時に1つのスレッドしかPythonコードを実行できないという制約です.これにより,Pythonでは単純なマルチスレッド化がCPUバウンドな処理の高速化に繋がりにくい特徴があります.
3. Pythonの非同期戦略:イベントループとスレッドプール
これまでの知識を前提に,Python(FastAPI)がどうやって効率的に動いているかを可視化します.
イベントループ (Event Loop) の動き
async def の世界です.
作業者(スレッド)は1人しかいません.しかし,I/O待ち(await)が発生すると,即座に別のタスクに切り替えて処理を進めます.これにより,待ち時間を極限まで減らします.
スレッドプール (ThreadPool) の動き
def(同期関数)の世界です.
イベントループとは別に,複数の作業者(スレッド)を用意します.タスクが来たら空いている作業者に割り振るため,物理的に複数の処理が同時に進みます.
4. FastAPIのアーキテクチャ
FastAPI(Uvicorn)は,上記のイベントループとスレッドプールを組み合わせて動作します.
- Worker(プロセス): リクエストを捌く独立した店舗のようなもの
-
Event Loop: 各Workerの中に1つだけ存在する司令塔.
async defの処理はここで行われる -
ThreadPool: 各Workerに付属する裏方集団.
defの処理はここへ飛ばされる
5. よくある間違いとベストプラクティス
FastAPIでやりがちなミスと,正しい実装方法を紹介します.
❌ 間違い:async def の中でブロッキング処理をする
async def はイベントループ上で動きます.ここで time.sleep や重い計算を行うと,イベントループ自体が停止し,サーバー全体がフリーズして他のリクエストを受け付けなくなります.
@app.get("/bad")
async def bad_handler():
time.sleep(5)
return {"msg": "これは危険"}
⭕️ 正解1:非同期ライブラリを使う
asyncio.sleep のように await 可能な非同期関数を使えば,待っている間に制御がイベントループに戻り,他のリクエストを処理できます.
import asyncio
@app.get("/good_async")
async def good_handler():
await asyncio.sleep(5)
return {"msg": "これはOK"}
⭕️ 正解2:同期処理なら def で定義する
def で定義された関数は,FastAPIが自動的にスレッドプールで実行してくれます.これにより,同期的なブロッキング処理(標準の sleep や requests など)を行ってもイベントループは止まりません.
@app.get("/good_sync")
def sync_handler():
time.sleep(5)
return {"msg": "これもOK"}
⭕️ 正解3:async 内でどうしても同期処理をしたい場合
計算処理など,どうしても async 関数内で重い処理を呼び出す必要がある場合は,run_in_threadpool を使って明示的にスレッドプールへ処理を逃がします.
from fastapi.concurrency import run_in_threadpool
@app.get("/manual")
async def manual_handler():
result = await run_in_threadpool(heavy_sync_function)
return {"result": result}
ちなみにdef 内で非同期処理をするとエラーになります.
まとめ
FastAPIにおける非同期処理のポイントは以下の3点です.
- 同期 vs 非同期: 「待ち時間をどう使うか」の違い.I/OバウンドなWebサーバーでは非同期が有利
- Workerの構造: 各Workerは「1つのEvent Loop」と「ThreadPool」を持っている
-
使い分け:
- 非同期対応ライブラリを使うなら
async def - 同期ライブラリやCPUバウンドな処理なら
def(FastAPIがスレッドプールでよしなにやってくれる)
- 非同期対応ライブラリを使うなら
この仕組みを理解していれば,「なぜかリクエストが詰まる」「CPU使用率が上がらない」といったトラブルに遭遇した際も,原因の切り分けがスムーズになるはずです.つよつよエンジニア目指して頑張りましょう!

