TD : Programmation asynchrone avec asyncio

Créez un fichier par exercice, par exemple td4_ex1.py. Ne nommez jamais un fichier asyncio.py, random.py ou httpx.py : Python importerait votre fichier à la place du vrai module.

Exercice 1 : Première coroutine

Écrivez une coroutine dire_bonjour(nom, delai) qui affiche Bonjour {nom}, attend delai secondes avec await asyncio.sleep(delai), puis affiche Au revoir {nom}.

  1. Lancez-la avec asyncio.run(...) pour "Alice" et un délai d’une seconde
  2. Retirez le await devant asyncio.sleep(delai) : que remarquez-vous ? Lisez bien le message affiché
  3. Remettez le await. Écrivez ensuite une coroutine main() qui appelle dire_bonjour("Alice", 1) sans await, et lancez asyncio.run(main()) : que se passe-t-il ?

Exercice 2 : Séquentiel contre concurrent

Reprenez la coroutine lire_capteur(nom, duree) du cours.

  1. Écrivez main_sequentiel() qui lit trois capteurs avec trois await l’un après l’autre (T1 : 2 s, T2 : 1 s, T3 : 3 s) et affiche la durée totale avec time.perf_counter()
  2. Écrivez main_concurrent() qui fait la même chose avec asyncio.gather
  3. Dans quel ordre les messages terminé s’affichent-ils ? Dans quel ordre sont les valeurs de la liste renvoyée par gather ? Pourquoi ?
  4. Complétez : la durée séquentielle est la __ des attentes, la durée concurrente est la __ des attentes

Exercice 3 : Un vrai appel HTTP

Jusqu’ici, les capteurs étaient simulés avec asyncio.sleep. Interrogeons maintenant une vraie API : Open-Meteo donne gratuitement la température actuelle d’un lieu, sans clé ni compte. Il suffit de lui envoyer les coordonnées GPS. Voici les quatre villes à interroger :

VILLES = [
    ("Marseille", 43.30, 5.37),
    ("Grenoble", 45.19, 5.72),
    ("Paris", 48.85, 2.35),
    ("Lille", 50.63, 3.06),
]
  1. Dans votre environnement virtuel, installez la bibliothèque httpx avec pip
  2. Testez un appel ordinaire (non asynchrone) :
import httpx

reponse = httpx.get(
    "https://api.open-meteo.com/v1/forecast",
    params={"latitude": 43.30, "longitude": 5.37, "current": "temperature_2m"},
)
print(reponse.json())

Dans le dictionnaire affiché, retrouvez la température actuelle. Quel est son chemin d’accès, de la forme ["..."]["..."] ?

  1. Écrivez une fonction ordinaire temperature(latitude, longitude) qui renvoie la température, puis main_sequentiel() qui affiche celle des 4 villes l’une après l’autre, avec la durée totale (time.perf_counter())
  2. Écrivez la version asynchrone : une coroutine temperature_async(client, latitude, longitude) qui utilise await client.get(...) à la place de httpx.get(...), puis main_concurrent() avec asyncio.gather. Le début de main_concurrent vous est donné :
async def main_concurrent():
    async with httpx.AsyncClient() as client:
        ...

async with ouvre le client et le referme proprement à la fin du bloc, comme with open(...) pour un fichier.

  1. Comparez les deux durées, puis relancez chaque programme plusieurs fois. Les durées changent-elles d’une exécution à l’autre ? Pourquoi, alors qu’avec asyncio.sleep elles étaient toujours identiques ?

Exercice 4 : Le piège de time.sleep

Dans lire_capteur, remplacez await asyncio.sleep(duree) par time.sleep(duree) et relancez main_concurrent().

  1. Quelle durée totale obtenez-vous ?
  2. Expliquez en deux phrases pourquoi gather ne sert plus à rien
  3. Remettez la bonne version

Exercice 5 : Collecte de capteurs avec logging

  1. Configurez logging en niveau INFO avec un format qui affiche l’heure, le niveau et le message
  2. Écrivez une coroutine lire_capteur(nom) qui attend un temps aléatoire entre 0,5 et 2 secondes (random.uniform(0.5, 2)), puis renvoie une température aléatoire entre 18 et 25 (round(random.uniform(18, 25), 1)). Elle enregistre la valeur avec logger.info
  3. Écrivez une fonction ordinaire moyenne(valeurs)
  4. Dans main(), lisez 5 capteurs (T1 à T5) avec gather, affichez la moyenne et la durée totale

Indice : construisez une liste de coroutines avec une boucle, puis passez-la à gather avec une étoile : await asyncio.gather(*taches). L’étoile * transforme la liste en arguments séparés.

  1. La durée totale est-elle proche de la somme des attentes ou de la plus longue ?

Exercice 6 : Capteur qui ne répond pas

Reprenez l’exercice 5. Le capteur T3 ne répond jamais : simulez-le avec une attente de 10 secondes.

  1. Écrivez lire_capteur_securise(nom, duree, timeout=2) qui appelle lire_capteur avec asyncio.wait_for. En cas de asyncio.TimeoutError, elle enregistre un logger.warning et renvoie None
  2. Dans main(), lisez les 5 capteurs (dont T3) avec gather
  3. Calculez la moyenne en ignorant les valeurs None, et affichez Moyenne sur N capteurs
  4. Quelle serait la moyenne si T3 était compté comme 0 ? Comparez avec la vraie moyenne
  5. Quelle est la durée totale du programme ? Pourquoi n’est-elle pas de 10 secondes ?

Exercice 7 (bonus) : Collecte périodique

Écrivez un programme qui répète la collecte de l’exercice 6 trois fois, avec une pause d’une seconde entre deux cycles (await asyncio.sleep(1)).

  1. Affichez le numéro du cycle avec logger.info au début de chaque cycle
  2. Lisez les heures dans les messages du journal : combien de temps dure un cycle ? Pourquoi ?
  3. Que se passerait-il si vous ajoutiez un sixième capteur qui met 20 secondes à répondre, avec timeout=2 ?

This site uses Just the Docs, a documentation theme for Jekyll.