Skip to content

kmprograms/event-loop-trap-gil-off

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🇬🇧 English version

Event Loop Trap & GIL-Off Demo

Praktyczna demonstracja trzech pułapek, w które wpada niemal każdy backend na FastAPI:

  1. Event Loop Trap — synchroniczny kod w async def blokuje cały serwer.
  2. ThreadPoolExecutor — poprawne odciążenie event loopa, ale…
  3. 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.

Wymagania

Narzędzie Wersja
Python 3.14t (no-GIL)
uv ≥ 0.7

Tip: Aby zainstalować free-threaded Python przez uv:

uv python install 3.14t

Szybki start

# Klonowanie i setup
git clone <repo-url> && cd event-loop-trap-gil-off
uv sync

# Uruchomienie serwera
uv run uvicorn app.main:app --reload

Serwer startuje na http://127.0.0.1:8000.

Endpointy

GET /ping

Health check. Zawsze odpowiada natychmiast — używaj go do sprawdzania, czy event loop jest wolny.

GET /heavy-blocking

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/ping

GET /heavy-good

Prawidł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/ping

GET /heavy-cpu?iterations=10000000

Puł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.

Benchmark

Skrypt scripts/cpu_bench.py automatyzuje pomiar:

uv run python scripts/cpu_bench.py --concurrency 8 --iterations 10_000_000

Wyś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%

Architektura

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

AgentService

Centralna klasa zarządzająca pulą wątków (ThreadPoolExecutor):

  • ainvoke_graph(query) — async wrapper wrzucający synchroniczną pracę na pulę wątków przez loop.run_in_executor().
  • acpu_burn(iterations) — symulacja CPU-bound workloadu (pure-Python pętla i * i).
  • shutdown() — graceful shutdown puli przy zamykaniu aplikacji.

Kluczowe wnioski

Event Loop Trap

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.

GIL i ThreadPoolExecutor

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.

Free-threaded Python (3.14t)

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.

Stos technologiczny

  • FastAPI — framework ASGI
  • Uvicorn — serwer ASGI
  • httpx — async HTTP client (benchmark)
  • Rich — formatowanie tabel w terminalu
  • uv — zarządzanie zależnościami i środowiskiem

Licencja

MIT

About

Event loop trap, ThreadPoolExecutor fix & GIL-off benchmark - FastAPI demo on Python 3.14t (free-threaded)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages