Aller au contenu
Développement medium

asyncio en Python : async, await et concurrence

9 min de lecture

logo python

asyncio est le module standard de Python pour la programmation asynchrone : il permet à un programme de faire autre chose pendant qu'il attend une opération lente. Quand votre code passe son temps à attendre le réseau, une API ou une base de données, asyncio lance des centaines de ces attentes en même temps, sur un seul thread, au lieu de les enchaîner. Ce guide couvre les mots-clés async/await, le lancement avec asyncio.run, l'exécution concurrente avec gather et TaskGroup, et surtout quand l'asynchrone est le bon outil.

Il s'adresse aux développeurs déjà à l'aise avec les fonctions qui veulent accélérer du code orienté entrées-sorties, ou comprendre le async/await qu'ils croisent dans des outils comme LiteLLM, les serveurs MCP ou les frameworks web. Tous les exemples ont été exécutés avec Python 3.12.

  • Comprendre ce que résout l'asynchrone (et ce qu'il ne résout pas).
  • Écrire une coroutine avec async def et l'attendre avec await.
  • Lancer du code asynchrone avec asyncio.run.
  • Exécuter des tâches en concurrence avec gather et TaskGroup.
  • Savoir quand choisir asyncio plutôt que du synchrone ou des threads.

Prenons un programme qui télécharge trois ressources, chacune prenant une seconde. En code synchrone classique, on attend la première, puis la deuxième, puis la troisième : trois secondes au total, alors que le processeur ne fait rien pendant ces attentes.

C'est le gaspillage que résout l'asynchrone : pendant qu'une tâche attend le réseau, le programme démarre les autres. Les trois attentes se chevauchent, et le total tombe à une seconde. L'asynchrone ne calcule pas plus vite : il attend plus intelligemment.

Une fonction déclarée avec async def n'est pas une fonction ordinaire : c'est une coroutine. L'appeler ne l'exécute pas, elle renvoie un objet qu'il faut attendre avec await.

import asyncio
async def telecharger(nom):
print(f"début {nom}")
await asyncio.sleep(1) # simule une E/S lente, sans bloquer
print(f"fini {nom}")
return f"{nom} ok"

Le point clé est await : il marque l'endroit où la coroutine peut rendre la main pendant qu'elle attend. Ici, await asyncio.sleep(1) simule une opération réseau d'une seconde. Pendant cette attente, la boucle d'événements peut faire progresser d'autres coroutines.

Une coroutine ne s'exécute pas toute seule : il faut une boucle d'événements (event loop), le moteur qui orchestre les coroutines. Le point d'entrée est asyncio.run(), appelé une seule fois tout en haut du programme.

async def principal():
resultat = await telecharger("A")
print(resultat)
asyncio.run(principal()) # démarre la boucle, exécute, puis la ferme

asyncio.run démarre la boucle, exécute la coroutine jusqu'au bout, puis referme proprement la boucle. À l'intérieur des coroutines, on enchaîne ensuite les appels avec await, sans jamais rappeler asyncio.run.

Attendre les coroutines une par une ne gagne rien : elles s'exécutent en série.

async def sequentiel():
await telecharger("A") # 1 s
await telecharger("B") # 1 s
await telecharger("C") # 1 s
# total : 3 secondes

Pour les lancer en même temps, on utilise asyncio.gather, qui démarre toutes les coroutines et attend l'ensemble de leurs résultats.

async def concurrent():
resultats = await asyncio.gather(
telecharger("A"),
telecharger("B"),
telecharger("C"),
)
return resultats
# total : 1 seconde

La différence est spectaculaire et mesurable : le passage de sequentiel() à concurrent() fait tomber le temps total de 3 secondes à 1 seconde, car les trois attentes se chevauchent. Plus vous avez d'appels à faire, plus le gain est grand.

TaskGroup : la concurrence structurée (Python 3.11)

Section intitulée « TaskGroup : la concurrence structurée (Python 3.11) »

Depuis Python 3.11, la façon recommandée de lancer plusieurs tâches est asyncio.TaskGroup, un gestionnaire de contexte qui attend automatiquement que toutes les tâches soient finies à la sortie du bloc.

async def principal():
async with asyncio.TaskGroup() as tg:
tg.create_task(telecharger("A"))
tg.create_task(telecharger("B"))
# à la sortie du bloc, les deux tâches sont terminées

Son avantage décisif sur gather : si une tâche échoue, toutes les autres sont annulées automatiquement et l'erreur est propagée proprement. C'est le principe de la concurrence structurée, plus sûr pour la gestion des erreurs dans du code réel.

Toujours depuis Python 3.11, asyncio.timeout annule une opération qui prend trop de temps, indispensable face à un service réseau qui ne répond pas.

async def principal():
try:
async with asyncio.timeout(0.5):
await telecharger("lent") # prend 1 s : sera annulé
except TimeoutError:
print("tâche trop lente, annulée")

C'est le point le plus mal compris de l'asynchrone. asyncio n'est pas une baguette magique de performance : son intérêt dépend de ce qui limite votre programme.

Votre programme est limité par...Bon outil
des entrées-sorties (réseau, API, base de données)asyncio
un calcul intensif (CPU : traitement d'images, maths)multiprocessing
un script simple et séquentielle synchrone classique

asyncio brille quand le programme passe son temps à attendre : des milliers d'appels d'API, un scraper, un serveur web. Il n'accélère pas un calcul qui sature le processeur, car tout tourne sur un seul thread.

  • asyncio sert à attendre efficacement des entrées-sorties, pas à calculer plus vite.
  • Une fonction async def est une coroutine : l'appeler ne l'exécute pas, il faut l'await.
  • await marque un point où la coroutine peut rendre la main pendant qu'elle attend.
  • On démarre tout avec asyncio.run(), appelé une seule fois au niveau supérieur.
  • gather et asyncio.TaskGroup (3.11) exécutent des tâches en concurrence : 3 attentes d'1 s prennent 1 s.
  • TaskGroup annule les autres tâches si l'une échoue (concurrence structurée) ; asyncio.timeout (3.11) borne la durée.
  • asyncio pour l'I/O, multiprocessing pour le CPU, synchrone pour un script simple.
  • Ne jamais mettre un appel bloquant dans une coroutine : cela gèle la boucle.

L'asynchrone prend tout son sens sur les appels réseau et se retrouve dans de nombreux outils modernes.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn