Pour ce TP, utilisez la branche suivante :
git checkout 6_starting_async_sync
Aller explorer le fichier async_sync_api.py, dedans 3 routes d'api sont définies :
time.sleepLe paramètre n est là pour illustrer une complexité métier de l'appel, plus n est grand plus l'appel est long.
Le sleep est un moyen de modéliser facilement un long calcul.
uv run fastapi dev demos/async_sync_api.py
Et tester avec un curl
curl localhost:8000/blocking/1
Pour cela, nous allons utiliser deux terminaux : 1 pour chaque client :
Lancer les commandes :
curl localhost:8000/blocking/15
time curl localhost:8000/blocking/1
time permet de mesurer le temps mis.
Combien de temps met le premier curl, et le deuxième ?
Que se passe-t-il si on inverse l'ordre des curls ?
Lancer les commandes :
curl localhost:8000/nonblocking/15
time curl localhost:8000/nonblocking/1
time permet de mesurer le temps mis.
Combien de temps met le premier curl, et le deuxième ?
Que se passe-t-il si on inverse l'ordre des curls ?
Lancer les commandes :
curl localhost:8000/sync/15
time curl localhost:8000/sync/1
time permet de mesurer le temps mis.
Combien de temps met le premier curl, et le deuxième ?
Il y a une parallélisation qui est faite au niveau des threads et non plus dans la boucle asyncio.
Essayons de réduire le nombre de threads pour illustrer cette différence.
Remplacer
app = FastAPI()
Par ce code qui passe de la valeur par défaut (40 threads) à 1 thread.
@asynccontextmanager
async def lifespan(app: FastAPI):
anyio.to_thread.current_default_thread_limiter().total_tokens = 1
yield
app = FastAPI(lifespan=lifespan)
Relancer les temps sur les méthodes de sync et non-blocking.
Le schéma ci-dessous montre, pour un worker uvicorn, qui exécute chaque type de route :
flowchart TB
Client["Client (curl)"]
subgraph W1["Worker uvicorn"]
Route1["/blocking, /nonblocking<br/>(async def)"]
Route2["/sync<br/>(def)"]
Route1 -->|"exécuté directement par"| EL1["Event loop asyncio<br/>1 seul thread"]
Route2 -->|"délégué à"| TP1
subgraph TP1["Threadpool"]
direction LR
T1a["Thread 1"]
T1b["Thread 2"]
T1c["Thread N"]
end
end
Client --> W1
Points clés à retenir :
/blocking et /nonblocking sont des coroutines (async def), donc l'event loop les exécute lui-même, sur son unique thread. /sync est une fonction classique (def), donc elle est déléguée à un thread du threadpool./blocking bloque ce thread avec time.sleep, tout le worker gèle./sync peuvent s'exécuter en parallèle, chacun dans son propre thread (limité par défaut à ~40 threads, ou 1 quand on le restreint plus bas dans ce TP).Les instructions du tp suivant sont ici