今月のPython 3.14リリースに伴い、遂にFree-Threadingが正式サポートされました。
今回はFastAPIでその恩恵を受けることが可能なのか、検証していきたいと思います。
Free Threadingはまだオプトインの機能であり、uvなどでは「3.14t」をインストールすることで正式にGILが無効化された版がインストールされます。
GILとは
GIL(Global Interpreter Lock)とは排他制御機構のことで、複数のスレッドが同時に実行されることを防ぎます。Rubyなどでも採用されています。
FastAPI x uvicornでの同期/非同期 処理
FastAPIが内部で利用するASGI FrameworkであるStarletteは通常とは異なる以下のような挙動をします。
-
async defエンドポイント- イベントループ内で実行(uvloop、asyncio event loop)
-
defエンドポイント- 外部スレッドプールで実行
- デフォルトでは外部スレッドプールのサイズは上限は40
- 外部スレッドプールで実行
ここで、以下のようなコードを準備して、python 3.14と3.14t(Free Threading)版でそれぞれ実行してみます。
- 2025年10月31日現在、httptoolのFree Threading対応版がリリースされていない?ためにmasterブランチの内容を取ってきています
- uvを用いる場合、uv自身のversionを最新版に更新する必要があるかもしれません
import asyncio
import sys
import time
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": f"Hello, World@python{sys.version_info.major}.{sys.version_info.minor}"}
@app.get("/sync_sleep")
def sync_sleep():
time.sleep(3)
return "Hello, World from sync endpoint!"
@app.get("/async_sleep")
async def async_sleep():
await asyncio.sleep(3)
return "Hello, World from async endpoint!"
@app.get("/sync_cpu")
def sync_cpu():
count = 0
for i in range(10**7):
count += i
return f"Sync CPU-bound result: {count}"
@app.get("/async_cpu")
async def async_cpu():
count = 0
for i in range(10**7):
count += i
return f"Async CPU-bound result: {count}"
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8050)
エンドポイントは以下です。
- /sync_sleep
- time.sleep()による同期IOバウンド処理
- /async_sleep
- asyncio.sleep()による非同期IOバウンド処理
- /sync_cpu
- 同期CPUバウンド処理
- /async_cpu
- 非同期CPUバウンド処理
time.sleepはCPUバウンド処理ではなくGILに影響されません
検証
Vegetaを使って負荷試験的なことをします。
Vegetaの設定項目
- duration: 10s
- max workers: 5
- rate 10
結果
結果を以下に示します。
それぞれの値は、 Latencyのmin, mean, 50 Percentile, 90 Percentile, max となっています。
| Version | 3.14 | 3.14t (Free Threading) |
|---|---|---|
| sync_sleep | 3.00s, 3.01s, 3.01s, 3.01s, 3.05s | 3.00s, 3.01s, 3.01s, 3.01s, 3.03s |
| async_sleep | 3.00s, 3.00s, 3.00s, 3.00s, 3.01s | 3.00s, 3.00s, 3.00s, 3.01s, 3.01s |
| sync_cpu | 366ms, 1.04s, 1.05s, 1.22s, 1.28s | 199ms, 205ms, 203ms, 209ms, 237ms |
| async_cpu | 226ms, 1.00s, 1.06s, 1.09s, 1.32s | 214ms, 957ms, 1.01s, 1.03s, 1.21s |
/sync_cpuのCPUバウンドな処理においては、GILの有無によって速度に大きな差が出ていることがわかります。
まとめ
sync_sleep, async_sleepに関しては、GILが影響しないため同期関数でも非同期関数もLatencyは大きく変わりません。
ただ、CPUバウンドであるsync_cpu、async_cpuに関しては、
- sync_cpu: GILの影響を受けない3.14tが大幅に速い
- async_cpu: IOバウンドな処理がないため、単一のリクエストが処理を占有し処理速度に変化なし
という結果になりました。
ただsync_**に関しては、外部スレッドプールの上限サイズが40という設定を変更せずに実行していることに留意してください。
Vegetaの実行結果
- 3.14: http://localhost:8050
- 3.14t : http://localhost:8051
$ echo 'GET http://localhost:8050/sync_sleep' | vegeta attack -duration=10s -max-workers=5 -rate=10 | tee results.bin | vegeta report
Requests [total, rate, throughput] 21, 1.74, 1.39
Duration [total, attack, wait] 15.069s, 12.065s, 3.003s
Latencies [min, mean, 50, 90, 95, 99, max] 3.002s, 3.008s, 3.007s, 3.008s, 3.026s, 3.047s, 3.047s
Bytes In [total, mean] 714, 34.00
Bytes Out [total, mean] 0, 0.00
Success [ratio] 100.00%
Status Codes [code:count] 200:21
Error Set:
$ echo 'GET http://localhost:8050/async_sleep' | vegeta attack -duration=10s -max-workers=5 -rate=10 | tee results.bin | vegeta report
Requests [total, rate, throughput] 21, 1.75, 1.40
Duration [total, attack, wait] 15.022s, 12.02s, 3.003s
Latencies [min, mean, 50, 90, 95, 99, max] 3.001s, 3.003s, 3.003s, 3.004s, 3.007s, 3.01s, 3.01s
Bytes In [total, mean] 735, 35.00
Bytes Out [total, mean] 0, 0.00
Success [ratio] 100.00%
Status Codes [code:count] 200:21
Error Set:
$ echo 'GET http://localhost:8051/sync_sleep' | vegeta attack -duration=10s -max-workers=5 -rate=10 | tee results.bin | vegeta report
Requests [total, rate, throughput] 21, 1.74, 1.40
Duration [total, attack, wait] 15.049s, 12.043s, 3.006s
Latencies [min, mean, 50, 90, 95, 99, max] 3.003s, 3.007s, 3.007s, 3.01s, 3.018s, 3.026s, 3.026s
Bytes In [total, mean] 714, 34.00
Bytes Out [total, mean] 0, 0.00
Success [ratio] 100.00%
Status Codes [code:count] 200:21
Error Set:
$ echo 'GET http://localhost:8051/async_sleep' | vegeta attack -duration=10s -max-workers=5 -rate=10 | tee results.bin | vegeta report
Requests [total, rate, throughput] 21, 1.75, 1.40
Duration [total, attack, wait] 15.019s, 12.015s, 3.004s
Latencies [min, mean, 50, 90, 95, 99, max] 3.001s, 3.004s, 3.003s, 3.007s, 3.009s, 3.012s, 3.012s
Bytes In [total, mean] 735, 35.00
Bytes Out [total, mean] 0, 0.00
Success [ratio] 100.00%
Status Codes [code:count] 200:21
Error Set:
$ echo 'GET http://localhost:8050/sync_cpu' | vegeta attack -duration=10s -max-workers=5 -rate=10 | tee results.bin | vegeta report
Requests [total, rate, throughput] 50, 4.90, 4.63
Duration [total, attack, wait] 10.788s, 10.213s, 574.996ms
Latencies [min, mean, 50, 90, 95, 99, max] 366.429ms, 1.041s, 1.053s, 1.217s, 1.251s, 1.284s, 1.284s
Bytes In [total, mean] 1950, 39.00
Bytes Out [total, mean] 0, 0.00
Success [ratio] 100.00%
Status Codes [code:count] 200:50
Error Set:
$ echo 'GET http://localhost:8050/async_cpu' | vegeta attack -duration=10s -max-workers=5 -rate=10 | tee results.bin | vegeta report
Requests [total, rate, throughput] 52, 4.99, 4.71
Duration [total, attack, wait] 11.052s, 10.417s, 634.503ms
Latencies [min, mean, 50, 90, 95, 99, max] 226.382ms, 1.002s, 1.058s, 1.085s, 1.088s, 1.315s, 1.32s
Bytes In [total, mean] 2080, 40.00
Bytes Out [total, mean] 0, 0.00
Success [ratio] 100.00%
Status Codes [code:count] 200:52
Error Set:
$ echo 'GET http://localhost:8051/sync_cpu' | vegeta attack -duration=10s -max-workers=5 -rate=10 | tee results.bin | vegeta report
Requests [total, rate, throughput] 100, 10.10, 9.90
Duration [total, attack, wait] 10.099s, 9.9s, 198.702ms
Latencies [min, mean, 50, 90, 95, 99, max] 198.702ms, 204.989ms, 202.967ms, 208.587ms, 213.43ms, 236.542ms, 237.451ms
Bytes In [total, mean] 3900, 39.00
Bytes Out [total, mean] 0, 0.00
Success [ratio] 100.00%
Status Codes [code:count] 200:100
Error Set:
$ echo 'GET http://localhost:8051/async_cpu' | vegeta attack -duration=10s -max-workers=5 -rate=10 | tee results.bin | vegeta report
Requests [total, rate, throughput] 55, 5.34, 4.95
Duration [total, attack, wait] 11.105s, 10.297s, 807.996ms
Latencies [min, mean, 50, 90, 95, 99, max] 214.247ms, 956.982ms, 1.007s, 1.034s, 1.036s, 1.197s, 1.205s
Bytes In [total, mean] 2200, 40.00
Bytes Out [total, mean] 0, 0.00
Success [ratio] 100.00%
Status Codes [code:count] 200:55
Error Set: