5.2 · Programmation asynchrone (asyncio)

Niveau 5 · Niveau pro & industrialisation

5.2Programmation asynchrone (asyncio)

Objectif : Gérer la concurrence avec async/await et asyncio.
Temps estimé : 15 min · lecture autonome · Si vous n'avez que 5 min : lisez « L'essentiel en 30s » + le schéma.

L'essentiel en 30 secondes

  • Async = exploiter l'attente : pendant que ça attend (API, base, fichiers), le programme fait autre chose au lieu de rester bloqué.
  • Le trio : async def déclare, await attend sans bloquer, gather lance ensemble — asyncio.run au sommet.
  • Mondes séparés : le bloquant (requests, sleep) paralyse l'async — libs async-compatibles ou rien.

Mini-cas filé — Nabil accélère ses collectes à Alger

Nabil, data analyst junior à Alger, récupère chaque matin des données depuis plusieurs sources web : en séquentiel, son script attend chaque réponse l'une après l'autre et la collecte dure une éternité. Il réécrit la boucle en coroutines lancées ensemble avec asyncio.gather : les attentes se recouvrent au lieu de s'additionner. Suite du cas 5.1 — Nabil occupe les temps morts au lieu de les subir.

Suite du cas 5.1 — Nabil passe du séquentiel qui attend au concurrent qui recouvre.

1. Quand l'attente domine : l'async

API, base de données, fichiers distants : le programme attend plus qu'il ne calcule. La programmation asynchrone libère le fil pendant l'attente : au lieu de rester bloqué, il passe à d'autres tâches, et un seul fil gère des milliers de requêtes concurrentes sans milliers de threads. Nabil y retrouve sa collecte matinale : le goulot est l'attente réseau, pas le calcul.

Frontière nette : pour du calcul pur (CPU-bound), l'async n'accélère rien — c'est multiprocessing. Async pour l'I/O, multiprocessing pour le CPU : choisir selon le goulot, pas selon la mode.

2. Le trio : async def, await, gather

async def fetch(url) déclare une coroutine, await attend sans bloquer (le fil est libéré pour d'autres tâches), asyncio.gather(*taches) lance ensemble et attend tout le monde. asyncio.run(main()) orchestre depuis le sommet via la boucle d'événements. Nabil lance ses collectes en gather : trois attentes se recouvrent au lieu de s'additionner.

Piège classique : oublier un await rend une coroutine jamais exécutée (avertissement discret, résultat vide). Chaque coroutine créée doit être attendue ou rassemblée.

3. Ne pas mélanger les mondes

Le bloquant dans l'async paralyse tout : time.sleep fige le fil, requests (synchrone) annule le bénéfice — utilisez asyncio.sleep et des libs async-compatibles (httpx, aiohttp). Un reste bloquant rescapé s'isole via run_in_executor. Nabil remplace requests par un client async : sans ça, son gather attendait quand même.

Règle d'or : async de bout en bout ou pas du tout — le mixte est le pire des mondes. Et on mesure avant/après pour prouver le gain au lieu de le supposer.

Erreur d'interprétation fréquente : « L'async rend tout plus rapide, et requests marche dedans ». Non. L'async n'accélère que les charges dominées par l'attente (I/O-bound) ; sur du calcul pur, il complique sans gain. Et requests, synchrone et bloquant, paralyse la boucle : dans l'async, on utilise des libs async-compatibles (httpx/aiohttp) — sinon on a payé la complexité sans toucher le gain.
Attendre ensemble Déclarer, attendre sans bloquer, lancer ensemble async def Déclarerla coroutineawait Attendresans bloquergather Lancer ensemble
Déclarer, attendre sans bloquer, lancer ensemble
Version texte du schéma (SEO / lecteurs d'écran / traduction) : 1) async def : Nabil déclare sa collecte de données à Alger en coroutines. 2) await : chaque attente réseau libère le fil au lieu de le bloquer. 3) gather : toutes les collectes partent ensemble — les attentes se recouvrent. Leçon : libs async-compatibles de bout en bout, et mesure du gain avant/après.

Le saviez-vous ?

Oublier un await crée une coroutine fantôme : elle existe mais ne s'exécute jamais — Python prévient d'un discret avertissement que beaucoup ignorent.
En async, l'oubli ne plante pas : il ne se passe rien.

Coroutine créée, jamais attendue : travail promis, jamais fait.

🔎 Exemple concret — Nabil collecte ses sources en parallèle

Les attentes se recouvrent au lieu de s'additionner.

import asyncio

async def collecter(source):
    print(f"collecte {source}...")
    await asyncio.sleep(1)
    return f"{source} : ok"

async def main(sources):
    return await asyncio.gather(*(collecter(s) for s in sources))

res = asyncio.run(main(["api", "fichier", "base"]))
print(res)

Lecture : trois attentes d'une seconde prennent environ une seconde au total, pas trois.

Leçon : en production réseau, remplacez le sleep par un client async (httpx/aiohttp).

Vocabulaire de la section

Programmation asynchrone
Modèle qui occupe le programme pendant les temps d'attente au lieu de le bloquer.

Exemple simple : Nabil collecte trois sources en même temps au lieu d'attendre chacune.

Ce que ce n'est pas : plusieurs processeurs qui calculent en parallèle.

Réflexe : mon programme attend-il plus qu'il ne calcule ?

async / await
async déclare une coroutine ; await attend sans bloquer le fil.

Exemple simple : Nabil await chaque collecte pour libérer le fil.

Ce que ce n'est pas : un appel bloquant qui fige tout.

Réflexe : ai-je oublié un await (coroutine fantôme) ?

asyncio.gather
Lance plusieurs coroutines ensemble et attend tout le monde.

Exemple simple : Nabil rassemble ses trois collectes en un seul gather.

Ce que ce n'est pas : une boucle qui attend chaque tâche l'une après l'autre.

Réflexe : ces attentes peuvent-elles se recouvrir ?

Coroutine
Fonction async suspendable et reprenable par la boucle d'événements.

Exemple simple : La fonction collecter de Nabil, suspendue pendant le sleep.

Ce que ce n'est pas : une fonction normale qui bloque jusqu'au bout.

Réflexe : qui attend cette coroutine ?

I/O-bound vs CPU-bound
Attente (réseau/disque) → asyncio ; calcul intensif → multiprocessing.

Exemple simple : Nabil prend asyncio pour ses API, pas pour un gros calcul.

Ce que ce n'est pas : async pour accélérer un calcul pur.

Réflexe : mon goulot est-il l'attente ou le calcul ?

En pratique — Découvrir async/await avec asyncio

  1. Écrivez une coroutine « async def tache(n) » qui affiche un message, attend (await asyncio.sleep(1)) puis affiche la fin.
  2. Lancez-la avec asyncio.run() et observez le comportement séquentiel d'une seule tâche.
  3. Lancez plusieurs tâches EN PARALLÈLE avec asyncio.gather() : constatez que 3 attentes d'1 s prennent ~1 s au total, pas 3.
  4. Concluez : l'asynchrone brille quand on ATTEND (réseau, I/O), pas pour accélérer un calcul intensif.
Vous comprenez async/await et asyncio, et savez quand l'asynchrone apporte un vrai gain (attentes I/O) : une compétence avancée recherchée.

Script prêt à dire — justifier l'async

« À Alger, notre collecte matinale attend chaque source l'une après l'autre : le goulot est l'attente réseau, pas le calcul. Je propose de passer la boucle en coroutines avec gather et un client async : les attentes se recouvriront. Je mesure avant/après pour prouver le gain, et je garde le calcul pur hors de l'async. »

À dire au responsable pour faire valider une réécriture ciblée, mesurée, sans toucher au calcul.

Modèle copier-coller — collecte async générique

import asyncio

async def tache(nom):
    print(f"debut {nom}")
    await asyncio.sleep(1)
    print(f"fin {nom}")
    return nom

async def main():
    return await asyncio.gather(*(tache(n) for n in ["a", "b", "c"]))

if __name__ == "__main__":
    print(asyncio.run(main()))
# En production reseau : remplacer asyncio.sleep par httpx/aiohttp (async-compatibles).

Solo / petit budget

Nabil seul : gather sur ses sources existantes suffit — pas de nouvelle infra, juste des coroutines.

TPE / équipe réduite

Client HTTP async partagé : toute l'équipe collecte sans multiplier les sessions bloquantes.

PME à Alger

Nabil mesure avant/après à Alger sur la collecte réelle : le gain prouvé justifie la réécriture.

Équipe qui démarre

Binôme : l'un convertit une source en async, l'autre traque les appels bloquants restants.

✓ Bonnes pratiques

  • Async pour l'I/O, multiprocessing pour le CPU : choisir selon le goulot.
  • gather pour lancer ensemble ; libs async-compatibles (httpx).
  • Mesurer avant/après : prouver le gain au lieu de le supposer.

✗ Pièges fréquents

  • Appeler requests (bloquant) dans l'async.
  • Oublier un await : coroutine fantôme jamais exécutée.
  • Mettre de l'async sur du calcul pur (aucun gain, complexité offerte).

Points clés à retenir

  • L'asynchrone occupe le programme pendant les temps d'attente au lieu de le bloquer.
  • async définit une coroutine ; await met en pause en attendant un résultat.
  • asyncio.gather() lance plusieurs tâches d'attente en parallèle.
  • asyncio sert les cas I/O-bound (réseau, disque), pas les calculs intensifs (→ multiprocessing).

Défi 48h — appliquez demain

Convertissez 1 boucle d'attente réelle (API, fichiers) en gather : les 3 attentes prennent-elles encore 3 fois le temps ?

Checklist sortie de section
  • Je peux expliquer async/await/gather en une phrase.
  • Ma démo gather recouvre les attentes (preuve mesurée).
  • Je sais pourquoi requests est banni dans l'async.
Auto-évaluation 1-5 : ai-je distingué attente et calcul avant de coder ?reste-t-il un appel bloquant dans ma chaîne async ?

Questions fréquentes

Asynchrone, threads, multiprocessing : quelle différence ?

Trois réponses à trois goulots. asyncio gère la concurrence d'attentes I/O dans un seul fil (idéal réseau/disque). Le threading partage aussi un processus mais convient à certains I/O bloquants. Le multiprocessing lance plusieurs processus pour paralléliser du calcul sur plusieurs cœurs. On choisit selon que le goulot est l'attente (asyncio) ou le calcul (multiprocessing).

L'asynchrone rend-il forcément un programme plus rapide ?

Non. Il accélère les charges dominées par l'attente (beaucoup de requêtes réseau simultanées). Pour un calcul pur (CPU-bound), il n'apporte rien, voire complique inutilement le code. Utilisez-le à bon escient, là où il y a des temps morts à exploiter — et mesurez pour le prouver.

Vérifiez votre compréhension

Dans quel cas asyncio apporte-t-il un gain ?

Ressources supplémentaires (optionnel, pour aller plus loin)

Examen du niveau 5Questions inédites et durcies pour valider vos acquis