Comprenez la concurrence coopérative d'asyncio : coroutines paresseuses, points de suspension, gather, TaskGroup et pièges des appels bloquants.
Ouvrir cette leçon dans Kodokonasyncio implémente de la concurrence coopérative sur un seul thread : une boucle d'événements orchestre des coroutines qui rendent volontairement la main à chaque await. Point fondamental, souvent mal compris : appeler une fonction async def n'exécute rien. Cela fabrique un objet coroutine inerte, dont le corps n'avance que lorsqu'il est attendu avec await ou planifié comme tâche sur la boucle.
import asyncio
async def greet() -> str:
return "hello"
coro = greet()
print(type(coro).__name__)
async def main() -> None:
print(await coro)
asyncio.run(main())await marque un point de suspension : la coroutine s'y gèle, la boucle reprend la main et fait progresser une autre tâche prête. Entre deux await, votre code s'exécute sans interruption - pas besoin de verrous pour la plupart des structures. Pour lancer plusieurs coroutines en parallèle (au sens I/O), enveloppez-les dans asyncio.gather : trois attentes d'une seconde chacune se terminent en une seconde au total, pas trois.
import asyncio
import time
async def fetch(name: str, delay: float) -> str:
await asyncio.sleep(delay)
return f"{name} done"
async def main() -> None:
start = time.perf_counter()
results = await asyncio.gather(
fetch("a", 1.0),
fetch("b", 1.0),
fetch("c", 1.0),
)
elapsed = time.perf_counter() - start
print(results)
print(f"total: {elapsed:.1f} s")
asyncio.run(main())Par défaut, gather propage la première exception levée mais laisse les autres tâches continuer en arrière-plan - rarement ce que vous voulez. Avec return_exceptions=True, les exceptions sont renvoyées comme résultats, à trier ensuite. Depuis Python 3.11, préférez asyncio.TaskGroup : à la première erreur, toutes les tâches sœurs sont annulées proprement, et les éventuelles exceptions multiples remontent groupées dans un ExceptionGroup, à intercepter avec except*.
import asyncio
import time
def blocking_io() -> str:
time.sleep(1)
return "report written"
async def main() -> None:
results = await asyncio.gather(
asyncio.to_thread(blocking_io),
asyncio.sleep(1, result="tick"),
)
print(results)
asyncio.run(main())fetch('a', 1.0) écrit seul, sans await ni create_task ?asyncio.sleep(1) lancés ensemble via asyncio.gather : quelle durée totale ?time.sleep(2) est-il proscrit dans une coroutine ?