Praktyczna demonstracja trzech pułapek, w które wpada niemal każdy backend na FastAPI:
- Event Loop Trap — synchroniczny kod w
async defblokuje cały serwer. - ThreadPoolExecutor — poprawne odciążenie event loopa, ale…
- GIL Lock — CPU-bound Python w wątkach nie skaluje się na rdzenie.
Projekt używa Python 3.14t (free-threaded), żebyś mógł porównać zachowanie z GIL-em i bez niego.
| Narzędzie | Wersja |
|---|---|
| Python | 3.14t (no-GIL) |
| uv | ≥ 0.7 |
Tip: Aby zainstalować free-threaded Python przez
uv:uv python install 3.14t
# Klonowanie i setup
git clone <repo-url> && cd event-loop-trap-gil-off
uv sync
# Uruchomienie serwera
uv run uvicorn app.main:app --reloadSerwer startuje na http://127.0.0.1:8000.
Health check. Zawsze odpowiada natychmiast — używaj go do sprawdzania, czy event loop jest wolny.
Pułapka nr 1. async def + time.sleep(5) — blokuje event loop na 5 sekund. W tym czasie /ping też nie odpowiada.
# Terminal 1 — odpal ciężki request
curl http://127.0.0.1:8000/heavy-blocking
# Terminal 2 — spróbuj ping w trakcie (czeka!)
curl http://127.0.0.1:8000/pingPrawidłowe rozwiązanie. Synchroniczna praca leci na ThreadPoolExecutor, event loop pozostaje wolny.
# Terminal 1
curl http://127.0.0.1:8000/heavy-good
# Terminal 2 — ping odpowiada natychmiast
curl http://127.0.0.1:8000/pingPułapka nr 2 — GIL. Pure-Python CPU burn w ThreadPoolExecutor. Z GIL-em 8 równoległych requestów trwa tyle co 8 sekwencyjnych. Bez GIL-a (Python 3.14t) — skaluje się na rdzenie.
Skrypt scripts/cpu_bench.py automatyzuje pomiar:
uv run python scripts/cpu_bench.py --concurrency 8 --iterations 10_000_000Wyświetla tabelę z czasami per wątek, a na końcu kluczowe metryki:
| Metryka | GIL ON | GIL OFF (3.14t) |
|---|---|---|
| T_single | ~X ms | ~X ms |
| T_parallel (8 req) | ~8× T_single | ~1× T_single |
| Parallel speedup | ~1.0× | ~N× (N ≈ rdzenie) |
| CPU efficiency | ~12% | ~80–100% |
event-loop-trap-gil-off/
├── app/
│ ├── main.py # FastAPI — endpointy i lifespan
│ └── agent_service.py # ThreadPoolExecutor + CPU burn logic
├── scripts/
│ └── cpu_bench.py # Klient benchmarkowy (httpx + rich)
├── pyproject.toml
└── uv.lock
Centralna klasa zarządzająca pulą wątków (ThreadPoolExecutor):
ainvoke_graph(query)— async wrapper wrzucający synchroniczną pracę na pulę wątków przezloop.run_in_executor().acpu_burn(iterations)— symulacja CPU-bound workloadu (pure-Python pętlai * i).shutdown()— graceful shutdown puli przy zamykaniu aplikacji.
async def + blokujący kod = zablokowany serwer. Rozwiązanie: loop.run_in_executor() lub def (sync) zamiast async def — wtedy FastAPI sam wrzuci to na thread pool.
ThreadPoolExecutor naprawia responsywność (event loop nie stoi), ale nie naprawia równoległości CPU. Z GIL-em wątki CPU-bound wykonują się szeregowo — jeden GIL na cały proces.
Python 3.14t wyłącza GIL. Wątki CPU-bound naprawdę działają równolegle. Ten sam kod, ta sama architektura — ale parallel speedup rośnie proporcjonalnie do liczby rdzeni.
- FastAPI — framework ASGI
- Uvicorn — serwer ASGI
- httpx — async HTTP client (benchmark)
- Rich — formatowanie tabel w terminalu
- uv — zarządzanie zależnościami i środowiskiem
MIT