W3docs

Python asyncio : async et await

Apprenez Python asyncio : coroutines, boucle d'événements, tâches, gather, délais et files — avec des exemples exécutables et des explications claires.

Le module asyncio de Python vous permet d'écrire du code concurrent dans un seul thread grâce aux mots-clés async et await. Au lieu de bloquer en attendant des réponses réseau ou des lectures de fichiers, un programme asyncio suspend la tâche en attente et passe immédiatement à un autre travail — reprenant l'exécution quand le résultat est prêt. C'est l'outil idéal pour les programmes liés aux entrées/sorties tels que les scrapers web, les clients API et les serveurs de messagerie.

Ce chapitre couvre :

  • Ce que sont les fonctions async (coroutines) et comment elles diffèrent des fonctions ordinaires
  • La boucle d'événements et la façon dont asyncio planifie le travail
  • L'attente de résultats, l'exécution de tâches en parallèle avec asyncio.gather et asyncio.create_task
  • La gestion des exceptions et des délais d'attente dans le code async
  • L'asyncio.Queue pour les modèles producteur-consommateur
  • Quand utiliser asyncio et quand préférer le threading

Pourquoi asyncio existe

Considérez un programme qui appelle deux API l'une après l'autre :

import time

def fetch(name, delay):
    time.sleep(delay)          # blocks the whole program
    return f'data from {name}'

start = time.perf_counter()
r1 = fetch('API A', 1)
r2 = fetch('API B', 1)
print(f'Done in {time.perf_counter() - start:.1f}s')
# Done in 2.0s

Les deux appels s'exécutent séquentiellement — 2 secondes au total, alors que chaque appel ne nécessite qu'1 seconde d'attente. Avec asyncio, le programme met en pause fetch('API A', ...) pendant qu'il attend, démarre immédiatement fetch('API B', ...), et les deux se terminent en environ 1 seconde :

import asyncio
import time

async def fetch(name, delay):
    await asyncio.sleep(delay)   # suspends only this coroutine
    return f'data from {name}'

async def main():
    start = time.perf_counter()
    r1, r2 = await asyncio.gather(fetch('API A', 1), fetch('API B', 1))
    print(f'Done in {time.perf_counter() - start:.1f}s')
    # Done in 1.0s

asyncio.run(main())

Coroutines : async def et await

Une fonction définie avec async def est appelée une fonction coroutine. L'appeler n'exécute pas immédiatement le corps — elle renvoie un objet coroutine qui doit être piloté par la boucle d'événements.

async def greet(name):
    print(f'Hello, {name}!')

# Calling it returns a coroutine object, nothing is printed yet
coro = greet('World')
print(type(coro))   # <class 'coroutine'>

# Run it properly
import asyncio
asyncio.run(greet('World'))
# Hello, World!

À l'intérieur d'une coroutine, await suspend l'exécution jusqu'à ce que l'objet awaitable (une autre coroutine, une Task ou un Future) produise un résultat. La boucle d'événements est libre d'exécuter d'autres coroutines pendant qu'une est suspendue.

import asyncio

async def step_one():
    print('Step 1: start')
    await asyncio.sleep(1)     # suspend for 1 second
    print('Step 1: end')
    return 'result-1'

async def main():
    value = await step_one()   # wait for step_one to finish
    print(value)

asyncio.run(main())
# Step 1: start
# Step 1: end
# result-1

Ce que vous pouvez attendre avec await

  • Une autre coroutine async def
  • Un asyncio.Task (créé avec asyncio.create_task)
  • Un asyncio.Future
  • Tout objet possédant une méthode __await__

Vous ne pouvez pas utiliser await en dehors d'une fonction async def.

La boucle d'événements

La boucle d'événements est le planificateur d'asyncio. Elle maintient une file de coroutines et de tâches, exécute chacune jusqu'à ce qu'elle atteigne un await, puis passe à l'élément suivant prêt à s'exécuter. Il y a généralement une boucle d'événements par thread.

asyncio.run(coro) est le point d'entrée standard pour les programmes asyncio. Il crée une nouvelle boucle d'événements, exécute la coroutine donnée jusqu'à sa complétion, ferme la boucle et retourne le résultat :

import asyncio

async def compute():
    await asyncio.sleep(0)   # yield control once
    return 6 * 7

result = asyncio.run(compute())
print(result)   # 42

Pour la plupart des applications, vous n'avez jamais besoin de gérer la boucle directement — asyncio.run gère la création et la destruction.

Exécuter des tâches en parallèle

asyncio.gather

asyncio.gather(*coroutines) planifie l'exécution simultanée de toutes les coroutines fournies et retourne leurs résultats dans le même ordre :

import asyncio

async def fetch_data(name, delay):
    print(f'Start fetching {name}')
    await asyncio.sleep(delay)
    print(f'Done fetching {name}')
    return f'data from {name}'

async def main():
    results = await asyncio.gather(
        fetch_data('API A', 1),
        fetch_data('API B', 2),
        fetch_data('API C', 1),
    )
    print(results)

asyncio.run(main())
# Start fetching API A
# Start fetching API B
# Start fetching API C
# Done fetching API A
# Done fetching API C
# Done fetching API B
# ['data from API A', 'data from API B', 'data from API C']

Les trois coroutines démarrent immédiatement. Le temps total écoulé correspond à la coroutine la plus lente (2 s), pas à la somme (4 s).

asyncio.create_task

asyncio.create_task(coro) enveloppe une coroutine dans une Task et la planifie pour s'exécuter prochainement. Contrairement à gather, la création d'une tâche la lance en arrière-plan pendant que la coroutine courante continue de s'exécuter :

import asyncio

async def background_job(name, delay):
    print(f'{name}: start')
    await asyncio.sleep(delay)
    print(f'{name}: end')
    return f'{name} done'

async def main():
    t1 = asyncio.create_task(background_job('Task A', 1))
    t2 = asyncio.create_task(background_job('Task B', 2))

    # Both tasks are already scheduled; await collects their results
    result1 = await t1
    result2 = await t2
    print(result1, result2)

asyncio.run(main())
# Task A: start
# Task B: start
# Task A: end
# Task B: end
# Task A done Task B done

Utilisez create_task lorsque vous souhaitez qu'une tâche démarre immédiatement et que vous prévoyez de récupérer son résultat (ou de l'annuler) plus tard. Utilisez gather lorsque vous voulez lancer un groupe fixe de coroutines et attendre qu'elles se terminent toutes ensemble.

Sortie entrelacée

Une façon utile de voir la boucle d'événements en action est d'observer comment les tâches s'entrelacent :

import asyncio

async def count_down(name, seconds):
    for i in range(seconds, 0, -1):
        print(f'{name}: {i}')
        await asyncio.sleep(1)
    print(f'{name}: done!')

async def main():
    await asyncio.gather(
        count_down('Task A', 3),
        count_down('Task B', 2),
    )

asyncio.run(main())
# Task A: 3
# Task B: 2
# Task A: 2
# Task B: 1
# Task A: 1
# Task B: done!
# Task A: done!

Les deux tâches partagent un seul thread ; la boucle d'événements alterne entre elles à chaque await asyncio.sleep(1).

Gestion des exceptions

Les exceptions levées dans une coroutine se propagent via await exactement comme dans le code synchrone. Utilisez un bloc try/except classique :

import asyncio

async def risky_task():
    await asyncio.sleep(0.1)
    raise ValueError('something went wrong')

async def main():
    try:
        await risky_task()
    except ValueError as e:
        print(f'Caught: {e}')

asyncio.run(main())
# Caught: something went wrong

Lors de l'utilisation d'asyncio.gather, si une coroutine lève une exception, les autres ne sont pas annulées par défaut, mais l'exception est relancée quand vous await l'appel à gather. Passez return_exceptions=True pour collecter les exceptions comme valeurs de retour à la place :

import asyncio

async def good():
    return 'ok'

async def bad():
    raise RuntimeError('oops')

async def main():
    results = await asyncio.gather(good(), bad(), return_exceptions=True)
    for r in results:
        if isinstance(r, Exception):
            print(f'Error: {r}')
        else:
            print(f'Result: {r}')

asyncio.run(main())
# Result: ok
# Error: oops

Délais d'attente avec asyncio.wait_for

asyncio.wait_for(coro, timeout) exécute une coroutine et l'annule si elle ne se termine pas dans le nombre de secondes imparti, en levant asyncio.TimeoutError :

import asyncio

async def slow_operation():
    await asyncio.sleep(5)
    return 42

async def main():
    try:
        result = await asyncio.wait_for(slow_operation(), timeout=1.0)
        print(result)
    except asyncio.TimeoutError:
        print('Timed out — operation cancelled')

asyncio.run(main())
# Timed out — operation cancelled

C'est important pour le code réseau en production où un serveur bloqué bloquerait autrement une tâche indéfiniment.

asyncio.Queue pour les modèles producteur-consommateur

asyncio.Queue est une file thread-safe et compatible async. Elle est idéale pour découpler les producteurs (code qui génère du travail) des consommateurs (code qui le traite) :

import asyncio

async def producer(queue):
    for i in range(1, 4):
        print(f'Produced item {i}')
        await queue.put(i)
        await asyncio.sleep(0.1)
    await queue.put(None)   # sentinel to signal consumers to stop

async def consumer(queue):
    while True:
        item = await queue.get()
        if item is None:
            break
        print(f'Consumed item {item}')

async def main():
    q = asyncio.Queue()
    await asyncio.gather(producer(q), consumer(q))

asyncio.run(main())
# Produced item 1
# Consumed item 1
# Produced item 2
# Consumed item 2
# Produced item 3
# Consumed item 3

Pour plusieurs consommateurs, utilisez queue.task_done() et queue.join() pour savoir quand tous les éléments ont été traités.

asyncio vs threading

asyncio et le module threading de Python permettent tous deux d'effectuer du travail en parallèle, mais de manière différente :

asynciothreading
Modèle de concurrenceCoopératif (les coroutines cèdent le contrôle à await)Préemptif (l'OS commute les threads)
Idéal pourDe nombreuses tâches liées aux E/S (réseau, disque)Tâches liées aux E/S utilisant des bibliothèques bloquantes
Travail lié au CPUPeu utile — toujours un seul threadPeu utile — le GIL limite le vrai parallélisme
SurchargeTrès faible (pas de threads OS)Plus élevée (chaque thread utilise des ressources OS)
État partagéSûr dans une boucle d'événementsNécessite des verrous pour éviter les courses aux données

Utilisez asyncio lorsque vous contrôlez le code d'E/S et pouvez utiliser des bibliothèques compatibles async (par ex. aiohttp, asyncpg). Utilisez threading lorsque vous vous appuyez sur des bibliothèques bloquantes tierces qui ne peuvent pas être rendues async.

Pour le vrai parallélisme CPU, tournez-vous plutôt vers multiprocessing ou concurrent.futures.ProcessPoolExecutor.

Pièges courants

Oublier await : Appeler une fonction async sans await retourne un objet coroutine et ne fait rien. Python émet un RuntimeWarning: coroutine '...' was never awaited pour aider à détecter cette erreur.

async def main():
    asyncio.sleep(1)   # BUG: returns a coroutine, does not sleep
    await asyncio.sleep(1)   # correct

Bloquer la boucle d'événements : L'exécution de code synchrone lent (une boucle intensive, un appel réseau bloquant, time.sleep) dans une coroutine gèle toute la boucle d'événements. Enveloppez les appels bloquants avec asyncio.to_thread (Python 3.9+) pour les exécuter dans un pool de threads sans bloquer :

import asyncio
import time

def blocking_task():
    time.sleep(2)   # simulates a slow blocking operation
    return 'done'

async def main():
    result = await asyncio.to_thread(blocking_task)
    print(result)

asyncio.run(main())
# done

Utiliser asyncio.run dans une boucle déjà en cours : Les notebooks Jupyter exécutent déjà une boucle d'événements. Utilisez directement await coro dans les cellules du notebook, ou installez nest_asyncio pour autoriser les boucles imbriquées.

Récapitulatif rapide

ModèleQuand l'utiliser
asyncio.run(main())Démarrer la boucle d'événements depuis du code synchrone
await coroExécuter une coroutine et attendre son résultat
asyncio.gather(*coros)Exécuter plusieurs coroutines en parallèle, collecter tous les résultats
asyncio.create_task(coro)Planifier une coroutine comme tâche en arrière-plan
asyncio.wait_for(coro, timeout=N)Ajouter une limite de temps à une coroutine
asyncio.QueueDécoupler les producteurs des consommateurs
asyncio.to_thread(fn)Exécuter une fonction bloquante sans geler la boucle

Exercice

Pratique
What does 'await asyncio.sleep(1)' do inside a coroutine?
What does 'await asyncio.sleep(1)' do inside a coroutine?
Was this page helpful?