Retour au catalogue
Season 10 20 Épisodes 1h 8m 2026

asyncio

v3.14 — Édition 2026. Une plongée en profondeur dans le framework asyncio de Python, couvrant l'event loop, les coroutines, la concurrence structurée, les primitives de synchronisation et les modèles asynchrones avancés. Pour Python 3.14.

Fondamentaux de Python Programmation asynchrone
asyncio
Lecture en cours
Click play to start
0:00
0:00
1
L'Event Loop et le Modèle Mental
Établissez votre modèle mental de base pour asyncio. Découvrez comment l'event loop agit comme un chef d'orchestre, gérant les tâches de manière coopérative sans dépendre du multithreading.
3m 30s
2
Coroutines vs Awaitables
Démystifiez les mots-clés async et await. Nous explorons la distinction cruciale entre une fonction coroutine et un objet coroutine, et ce qui se passe réellement lorsque vous faites un await sur une opération.
3m 25s
3
Le Point d'Entrée asyncio.run()
Découvrez comment amorcer une application asyncio en toute sécurité. Nous discutons d'asyncio.run, de l'arrêt des executors et du gestionnaire de contexte Runner pour les cycles de vie complexes de l'event loop.
3m 30s
4
Planification avec les Tasks
Apprenez à exécuter des opérations simultanément en utilisant asyncio.create_task(). Nous découvrons les conséquences graves du garbage collection sur les Tasks non référencées.
3m 04s
5
Concurrence Structurée avec les TaskGroups
Maîtrisez la concurrence structurée. Comprenez comment asyncio.TaskGroup gère en toute sécurité de multiples opérations concurrentes et garantit des nettoyages propres lorsque des exceptions se produisent.
3m 03s
6
Annulation de Tasks et Timeouts
Explorez la mécanique d'interruption des opérations. Apprenez pourquoi asyncio.CancelledError est levée, comment la gérer dans un bloc finally, et pourquoi vous ne devriez jamais l'ignorer silencieusement.
3m 20s
7
Céder le Contrôle avec Sleep
Comprenez le véritable but d'asyncio.sleep(0). Découvrez comment céder le contrôle empêche les boucles gourmandes en CPU d'affamer l'event loop et de geler l'application.
3m 14s
8
Synchronisation : Locks et Mutexes
Prévenez les race conditions dans le code async. Nous explorons asyncio.Lock, discutons de sa nature non thread-safe, et montrons pourquoi les locks de threading gèleront votre event loop.
3m 36s
9
Coordonner l'État avec les Events
Apprenez à diffuser des signaux à plusieurs tâches en attente. Nous expliquons comment asyncio.Event et asyncio.Condition remplacent élégamment les boucles de polling inefficaces.
3m 52s
10
Limiter la Concurrence avec les Semaphores
Protégez les ressources fragiles et évitez les bannissements liés au rate-limiting. Découvrez comment asyncio.Semaphore limite l'exécution concurrente sans bloquer votre architecture.
3m 35s
11
Workflows Producteur-Consommateur
Découplez les producteurs rapides des consommateurs lents en toute sécurité. Explorez asyncio.Queue, la signalisation de fin de tâche et les nouvelles mécaniques de shutdown pour les Queues.
3m 20s
12
Réseau de Haut Niveau avec les Streams
Plongez dans les IO Streams de haut niveau. Nous discutons de StreamReader, StreamWriter, et pourquoi omettre await writer.drain() peut détruire silencieusement la mémoire de votre serveur.
3m 47s
13
Créer des Serveurs Async
Construisez des serveurs réseau hautement concurrents. Apprenez comment asyncio.start_server abstrait les connexions clientes, en générant une tâche isolée pour chaque pair.
3m 19s
14
Subprocesses Non-bloquants
Exécutez des commandes shell de manière asynchrone. Découvrez pourquoi l'utilisation du module subprocess standard arrête l'event loop, et comment asyncio.create_subprocess_exec corrige cela.
3m 18s
15
Futures : La Passerelle de Bas Niveau
Déballez les fondations des instructions await. Nous examinons asyncio.Future, son rôle en tant que résultat éventuel, et comment il fait le pont entre le code hérité basé sur les callbacks et la syntaxe moderne.
3m 36s
16
Transports et Protocols
Passez sous le capot pour voir comment asyncio communique avec l'OS. Comprenez la relation 1:1 pilotée par les callbacks entre les Transports (comment les octets se déplacent) et les Protocols (ce que signifient les octets).
3m 51s
17
Le Threading dans un Monde Async
Faites le pont entre les mondes synchrone et asynchrone. Apprenez comment décharger du code bloquant lourd en toute sécurité en utilisant des executors et des callbacks thread-safe sans bloquer l'event loop.
3m 16s
18
Générateurs Async et Nettoyage
Évitez les fuites de ressources avec les générateurs async. Nous explorons pourquoi l'itération 'async for' peut laisser des connexions pendantes lorsqu'elle est interrompue, et comment aclosing() offre de la sécurité.
3m 32s
19
Maîtriser le Mode Debug
Attrapez les bugs de concurrence instantanément. Apprenez à utiliser PYTHONASYNCIODEBUG pour profiler les callbacks lents, dénicher les coroutines sans await, et identifier les exceptions jamais récupérées.
2m 59s
20
Extension et Custom Loops
Le grand final. Nous explorons l'intégration avancée et ce qu'il faut pour écrire une event loop personnalisée ou sous-classer BaseEventLoop pour des environnements spécialisés et à haute performance.
3m 39s

Épisodes

1

L'Event Loop et le Modèle Mental

3m 30s

Établissez votre modèle mental de base pour asyncio. Découvrez comment l'event loop agit comme un chef d'orchestre, gérant les tâches de manière coopérative sans dépendre du multithreading.

Télécharger
Salut, c'est Alex de DEV STORIES DOT EU. asyncio, épisode 1 sur 20. Beaucoup de développeurs entendent le mot asynchrone et supposent que leur code va s'exécuter en parallèle sur plusieurs cœurs de CPU. Mais quand ils inspectent leur application, ils se rendent compte qu'elle tourne entièrement sur un seul thread. Le secret de cette efficacité sans vrai parallélisme, c'est l'event loop, et comprendre son modèle mental est la base d'asyncio. L'event loop est le gestionnaire d'exécution central de toute application asyncio. C'est exactement ce que son nom indique : une boucle continue qui vérifie les opérations prêtes à tourner, les exécute, puis cherche l'opération suivante. Il est vital de séparer ce concept du multithreading. Dans un programme multithread, c'est l'OS qui contrôle l'exécution. L'OS va mettre un thread en pause de force et passer à un autre pour partager le temps de CPU. Les threads eux-mêmes n'ont aucun contrôle sur le moment où ils sont mis en pause. Ça demande un overhead système important pour gérer les context switches et protéger la mémoire partagée. L'event loop fonctionne sur un modèle complètement différent qu'on appelle le multitâche coopératif. Tout tourne de façon séquentielle sur un seul thread. La loop n'interrompt jamais une opération. À la place, elle compte sur le code pour faire un yield explicite et rendre le contrôle à la loop quand il doit attendre quelque chose. Imagine l'event loop comme un seul chef cuisinier expert dans la cuisine d'un restaurant bondé. Le chef reçoit plusieurs commandes en même temps. S'il met une grande marmite de bouillon à mijoter sur le feu, il ne reste pas planté devant à fixer le liquide jusqu'à ce que ce soit fini. Cette approche bloquerait toute la cuisine et rien d'autre ne cuirait. À la place, le chef allume le feu, laisse la marmite mijoter, et passe immédiatement à la découpe des légumes pour un autre plat. Le chef représente le thread d'exécution unique. L'event loop, c'est le chef qui scanne continuellement la cuisine, qui sait exactement quelles marmites mijotent, quelles poêles doivent être retournées, et qui passe instantanément au job suivant. Dans ton logiciel, une marmite qui mijote, c'est généralement une opération d'input ou d'output. Quand ton code envoie une request à une base de données, la base de données prend du temps pour traiter la query et renvoyer les données. Un programme synchrone traditionnel se figerait et attendrait la réponse. Avec une event loop, l'opération enregistre sa request puis dit à la loop qu'elle est en attente. L'event loop passe immédiatement à un autre bout de code qui a des données prêtes à être traitées. Quand la base de données répond enfin, l'opération d'origine signale à l'event loop qu'elle est prête à reprendre. L'event loop la replace dans la queue et reprendra son exécution dès que le job en cours fera un yield. Voici l'idée clé. Vu que l'event loop ne peut pas arrêter une opération de force, tout le système repose entièrement sur la coopération. Si un job décide de faire un énorme calcul mathématique sans jamais faire de yield pour rendre le contrôle, l'event loop s'arrête. Le thread unique est occupé. Dans notre cuisine, c'est le chef qui décide de moudre manuellement un énorme sac de farine en ignorant tous les autres plats. Les marmites qui mijotent débordent, les nouvelles commandes s'accumulent, et la cuisine est paralysée. La loop est seulement aussi efficace que le code qui tourne à l'intérieur. La vraie efficacité asynchrone ne vient pas du fait de faire plusieurs calculs au même moment physique, mais de s'assurer que ton thread unique ne gaspille jamais une seule milliseconde à ne rien faire en attendant le monde extérieur. Si tu veux nous aider à continuer l'émission, tu peux nous soutenir en cherchant DevStoriesEU sur Patreon. Merci pour ton écoute, et happy coding à tous !
2

Coroutines vs Awaitables

3m 25s

Démystifiez les mots-clés async et await. Nous explorons la distinction cruciale entre une fonction coroutine et un objet coroutine, et ce qui se passe réellement lorsque vous faites un await sur une opération.

Télécharger
Salut, ici Alex de DEV STORIES DOT EU. asyncio, épisode 2 sur 20. Tu écris une fonction, tu l'appelles, et absolument rien ne se passe. Ton code s'exécute sans erreur, mais la base de données est vide et la requête réseau n'est jamais envoyée. Le problème vient d'une incompréhension fondamentale de ce que fait réellement l'appel d'une fonction asynchrone. Aujourd'hui, nous allons nous intéresser aux coroutines et aux awaitables. En Python classique, quand tu appelles une fonction standard, elle s'exécute immédiatement. Les fonctions asynchrones dérogent complètement à cette règle. Il y a une différence stricte entre une fonction coroutine et un objet coroutine. Quand tu écris async def, tu crées une fonction coroutine. Quand tu appelles cette fonction dans ton code, ça n'exécute pas le corps de la fonction. À la place, ça renvoie un objet coroutine. Imagine que tu commandes un café. La fonction async def est l'élément du menu. L'appeler, c'est comme passer ta commande à la caisse. Tu reçois un ticket. Ce ticket est ton objet coroutine. Tu as exprimé ton intention, mais tu n'as pas encore ta boisson, et personne n'a même commencé à la préparer. Pour vraiment déclencher la préparation et obtenir ton café, tu dois patienter au comptoir. En Python, tu fais ça en utilisant le mot-clé await. Quand tu tapes await suivi de cet objet coroutine, deux choses distinctes se produisent. Premièrement, la coroutine commence enfin à exécuter son code interne. Deuxièmement, la fonction où tu as placé le await se met complètement en pause. Elle rend le contrôle à Python, en indiquant qu'elle ne peut pas continuer tant que cette coroutine spécifique n'est pas terminée. Ce comportement de pause est la principale différence mécanique de la programmation asynchrone. Pendant que ta fonction est en pause en attendant le café, Python est libre d'aller exécuter d'autre code ailleurs. Ceci nous amène au terme plus large de awaitable. Un awaitable est simplement n'importe quel objet que Python te permet d'utiliser avec le mot-clé await. Toutes les coroutines sont des awaitables. Quand tu vois await, lis-le comme une commande directe : exécute cet objet awaitable jusqu'à la fin, et suspends mon exécution en cours jusqu'à ce qu'il renvoie un résultat final. Si tu écris une fonction asynchrone appelée fetch data, le simple fait d'appeler fetch data renvoie l'objet coroutine. Si tu assignes cet appel à une variable nommée pending request, cette variable contient juste la coroutine non exécutée. Le réseau reste complètement silencieux. Plus tard dans ton script, quand tu écris await pending request, Python exécute enfin l'appel réseau. L'exécution de ton bloc de code actuel s'arrête exactement à cette ligne. Une fois que le serveur répond, l'expression await se résout avec les données renvoyées, et le code qui l'entoure passe à la ligne suivante. Voici l'idée clé. Tu ne peux utiliser le mot-clé await qu'à l'intérieur d'une fonction async def. Parce que faire un await sur un objet nécessite de mettre en pause l'exécution en cours, la fonction qui contient le await doit elle-même pouvoir être mise en pause. C'est pourquoi le comportement asynchrone se propage vers l'extérieur. Pour faire un await sur une coroutine, tu dois être à l'intérieur d'une coroutine. Tu construis une chaîne d'opérations suspendues, qui attendent toutes que la tâche de plus bas niveau se termine. N'oublie pas, appeler une fonction asynchrone sans faire de await dessus revient juste à générer un reçu pour un travail que tu n'as jamais vraiment demandé à personne de faire. Le code ne s'exécutera jamais tant que tu ne fais pas un await dessus. Merci d'avoir écouté. À la prochaine !
3

Le Point d'Entrée asyncio.run()

3m 30s

Découvrez comment amorcer une application asyncio en toute sécurité. Nous discutons d'asyncio.run, de l'arrêt des executors et du gestionnaire de contexte Runner pour les cycles de vie complexes de l'event loop.

Télécharger
Salut, c'est Alex de DEV STORIES DOT EU. asyncio, épisode 3 sur 20. Mal utiliser le point de départ de ton application asynchrone peut laisser derrière des thread executors orphelins et des async generators non fermés. Pour éviter les fuites de ressources cachées, tu dois utiliser le bon outil pour démarrer et arrêter ton application, ce qui nous amène à l'entry point asyncio.run. Beaucoup de développeurs essaient par erreur d'utiliser cet outil pour exécuter des coroutines individuelles au hasard depuis du code synchrone. Ce n'est pas son but. Tu ne peux pas appeler la fonction run quand une autre event loop asyncio tourne déjà dans le même thread. Faire ça déclenche immédiatement une runtime error. Elle est conçue spécifiquement pour être l'entry point unique et de haut niveau d'un programme. Imagine l'initialisation de la main loop d'un serveur web qui coordonne toutes les requêtes de trafic entrantes. Tu as une fonction asynchrone centrale qui se bind à un port réseau, configure les request handlers, et garde le serveur en vie. Tu passes cette unique fonction main à la fonction run. Quand tu fais ça, asyncio gère automatiquement tout le cycle de vie de l'event loop. D'abord, il crée une nouvelle event loop et la définit comme la loop active actuelle pour le thread. Ensuite, il exécute la coroutine main de ton serveur web jusqu'à ce qu'elle se termine. Voici le point clé. Le travail le plus important que fait cette fonction se passe après que ton code main a fini de s'exécuter. Elle fait un nettoyage rigoureux. Avant de rendre le contrôle à la partie synchrone de ton programme, elle annule toutes les pending tasks restantes. Ensuite, elle ferme proprement les background threads dans l'executor par défaut. Enfin, elle finalise tous les async generators avant de fermer complètement l'event loop. Tu peux aussi passer un debug flag à cette fonction, ce qui force la loop sous-jacente à tourner en debug mode pour aider à tracer les problèmes d'exécution. Comme cette fonction standard démonte tout à la fin, elle crée une frontière stricte. Si tu as un scénario où tu dois exécuter plusieurs blocs asynchrones distincts depuis du code synchrone, mais que tu veux qu'ils partagent la même event loop, appeler la fonction run standard à la suite va échouer, parce qu'une nouvelle loop est créée et détruite à chaque fois. Pour cette situation, tu utilises le context manager Runner d'asyncio. Tu ouvres un bloc de contexte en utilisant le with statement standard de Python. Entrer dans ce bloc initialise l'event loop. Une fois à l'intérieur, tu peux appeler la méthode run de l'objet runner. Tu lui passes une coroutine, il l'exécute jusqu'à la fin, et retourne le résultat. Tu peux appeler cette méthode run interne plusieurs fois dans le même bloc de contexte. L'event loop reste en vie, conservant l'état, les données en cache et les connexions entre ces appels séparés. Tu peux configurer le context manager quand tu le crées en passant un debug flag, ou même une loop factory personnalisée si ton environnement nécessite une implémentation d'event loop spécialisée. Quand l'exécution quitte enfin le bloc du context manager, le runner exécute exactement la même séquence de teardown que la fonction autonome. Il nettoie les executors, finalise les generators, et ferme la loop en toute sécurité. La stabilité de ton application dépend entièrement de la façon dont elle démarre et se termine. Que tu utilises un simple appel de fonction ou le context manager, faire passer ton exécution par ces entry points officiels est le seul moyen de garantir que tes ressources asynchrones sont fiablement nettoyées quand le programme se termine. C'est tout pour cette fois. Merci d'avoir écouté, et continue de développer !
4

Planification avec les Tasks

3m 04s

Apprenez à exécuter des opérations simultanément en utilisant asyncio.create_task(). Nous découvrons les conséquences graves du garbage collection sur les Tasks non référencées.

Télécharger
Salut, c'est Alex de DEV STORIES DOT EU. asyncio, épisode 4 sur 20. Tu lances un background process pour envoyer des métriques système. Tu checkes ton dashboard plus tard, et la moitié des données a disparu. Aucune erreur n'a été levée. Ton code s'est juste arrêté silencieusement en pleine exécution. Ça arrive parce que tu as traité ton background job comme du fire-and-forget. Aujourd'hui, on parle du scheduling avec les Tasks, et pourquoi tu dois toujours garder une référence aux choses que tu crées. Quand tu as une coroutine que tu veux exécuter en parallèle avec d'autres bouts de code, tu utilises la fonction create task d'asyncio. Tu passes ta coroutine à cette fonction, et asyncio l'encapsule dans un objet Task. Ça dit à l'event loop de scheduler la Task pour exécution. La fonction te retourne immédiatement le nouvel objet Task, ce qui permet à ton programme principal de continuer à tourner pendant que la Task s'exécute en background. Beaucoup de développeurs appellent create task et ignorent la valeur de retour. C'est un énorme piège. Voici le point clé. L'event loop d'asyncio ne garde que des weak references vers les Tasks qu'elle exécute. La loop elle-même ne protège pas ta Task du garbage collector de Python. Si tu n'assignes pas l'objet Task retourné à une variable ou que tu ne le stockes pas dans une structure de données, le garbage collector finira par remarquer qu'aucune hard reference n'existe. Quand ça arrive, Python détruit l'objet Task. Il se fiche de savoir si la coroutine est en plein milieu de l'exécution d'une database query ou en train d'attendre une network response. La Task disparaît, tout simplement. Imagine une fonction async appelée ship metrics. Elle formate un payload de données et envoie une HTTP request à un serveur externe. Tu appelles create task et tu lui passes ship metrics, mais tu n'assignes le résultat à rien. La Task commence à tourner. Elle formate le payload. Ensuite, elle arrive au network call et se met en pause pour attendre une connexion. Pendant qu'elle est en pause, le garbage collector s'exécute. Le strong reference count est à zéro. La Task est détruite. Le serveur ne reçoit jamais le payload, et ton application ne logge aucune erreur parce que l'exécution a tout simplement cessé d'exister. Pour éviter ça, tu dois toujours garder une strong reference vers les Tasks que tu schedules. Si tu crées une seule Task, assigne-la à une variable. Si tu schedules plusieurs background tasks dans une loop, ajoute-les à un set ou une list Python standard. Tant que ce set existe en mémoire, les strong references existent, et le garbage collector laissera tes Tasks en cours d'exécution tranquilles. Tu peux ensuite utiliser un callback pour retirer la Task de ton set une fois qu'elle est terminée. La fonction create task accepte aussi quelques arguments optionnels. Tu peux passer une string au paramètre name, ce qui attribue un identifiant spécifique à la Task. C'est fortement recommandé pour le debugging, car ça rend beaucoup plus facile de retrouver quelle opération spécifique a échoué si une exception est levée plus tard. Tu peux aussi passer un argument context pour établir un état de context variable spécifique pour la Task. Traiter les background operations comme du fire-and-forget finira par te retomber dessus avec des silent failures. Si tu demandes à l'event loop d'exécuter quelque chose, tu dois garder une hard reference vers l'objet résultant jusqu'à ce que le travail soit complètement terminé. Merci d'avoir écouté, happy coding à tous !
5

Concurrence Structurée avec les TaskGroups

3m 03s

Maîtrisez la concurrence structurée. Comprenez comment asyncio.TaskGroup gère en toute sécurité de multiples opérations concurrentes et garantit des nettoyages propres lorsque des exceptions se produisent.

Télécharger
Salut, c'est Alex de DEV STORIES DOT EU. asyncio, épisode 5 sur 20. Avant Python 3.11, lancer plusieurs tâches concurrentes était facile, mais les gérer de manière sûre quand l'une d'elles plantait était notoirement difficile. Tu te retrouvais souvent avec des tâches en background orphelines qui gaspillaient silencieusement des ressources. La solution à ce bazar, c'est la concurrence structurée avec TaskGroup. Un TaskGroup est un context manager asynchrone. On le confond parfois avec une liste de tâches classique, mais c'est beaucoup plus strict. Il offre des garanties de sécurité fortes sur la façon dont les tâches démarrent et se terminent. Il impose une règle : une routine parente ne peut pas se terminer tant que toutes ses opérations enfants ne sont pas terminées ou proprement annulées. Tu l'utilises en ouvrant un bloc async with. À l'intérieur de ce bloc, tu appelles la méthode create task directement sur l'objet group pour démarrer tes opérations concurrentes. Tu ne balances pas ces tâches dans un array classique pour faire un await manuel. À la place, quand le code atteint la fin du bloc async with, le TaskGroup se met implicitement en pause. Il attend juste là jusqu'à ce que chaque tâche lancée se termine. Le bloc ne va tout simplement pas se terminer en avance. Voici le point clé. La vraie puissance d'un TaskGroup réside dans sa façon de gérer les erreurs. Avec des outils legacy comme gather, si tu lançais plusieurs tâches et que l'une d'elles levait une erreur, les autres continuaient de tourner en background. Tu devais écrire une logique de gestion d'erreurs complexe pour traquer les survivantes et les tuer. Un TaskGroup gère ça automatiquement. Prends le scénario d'un web scraper qui récupère trois endpoints d'API distincts en même temps. Tu as besoin des données utilisateur, des posts récents et des alertes système. Tu ouvres un TaskGroup et tu lances trois tâches. Elles commencent toutes à tourner de façon concurrente sur le réseau. Au beau milieu de l'opération, l'endpoint des posts récents fait un timeout et lève une erreur de connexion. Le TaskGroup détecte immédiatement cet échec. Il intercepte l'erreur et envoie automatiquement un signal d'annulation à la tâche des données utilisateur et à celle des alertes système. Il nettoie ces opérations en attente pour qu'elles ne continuent pas de consommer de la bande passante réseau ou de la mémoire. Les tâches restantes lèvent une cancelled error en interne, pour accuser réception de l'arrêt. Une fois que toutes les tâches restantes sont arrêtées en toute sécurité, le TaskGroup regroupe l'erreur de connexion initiale dans une nouvelle structure appelée ExceptionGroup, et la lève en dehors du bloc de contexte. Ce comportement rend ton code asynchrone entièrement prévisible. Si l'exécution dépasse le bloc avec succès, tu sais de source sûre que chaque tâche a réussi. Si le bloc lève un ExceptionGroup, tu sais que l'échec a été attrapé et que tout le reste a été correctement arrêté. Tu ne laisses jamais de tâches rebelles tourner en background. Si tu as besoin des résultats des tâches qui ont réussi, tu peux les récupérer directement depuis les objets task que tu as créés, à condition qu'elles se soient terminées avant que l'erreur ne se produise. En liant les tâches à un bloc de cycle de vie strict, les TaskGroups garantissent que les opérations concurrentes entrent et sortent de ton application comme une seule unité coordonnée. C'est tout pour aujourd'hui. Merci d'avoir écouté — va construire un truc cool.
6

Annulation de Tasks et Timeouts

3m 20s

Explorez la mécanique d'interruption des opérations. Apprenez pourquoi asyncio.CancelledError est levée, comment la gérer dans un bloc finally, et pourquoi vous ne devriez jamais l'ignorer silencieusement.

Télécharger
Salut, c'est Alex de DEV STORIES DOT EU. asyncio, épisode 6 sur 20. Tu as écrit un error handler robuste, qui catch toutes les exceptions génériques dans ton worker async. Mais maintenant, pendant le shutdown, ton event loop se retrouve bouchée par des tasks zombies qui refusent de mourir. Ton filet de sécurité les garde en fait en vie. C'est exactement ce qu'on va voir aujourd'hui : la cancellation de tasks et les timeouts. Quand tu as besoin d'arrêter une task en cours d'exécution, tu appelles sa méthode cancel. Ça ne termine pas instantanément la task comme quand on kill un process système. À la place, asyncio demande un arrêt en injectant une erreur, plus précisément une asyncio CancelledError, dans la task. Cette erreur est raised exactement au point d'await actuel ou suivant de la task. La coroutine déroule ensuite sa stack exactement comme elle le ferait pour n'importe quelle erreur Python standard. Ce mécanisme est aussi la base des timeouts. Quand tu wrap une task dans une fonction de timeout et que le timer expire, l'event loop n'arrête pas la task par magie. Elle appelle simplement cancel sur cette task. La task reçoit la CancelledError à son prochain await, déroule son state, et finit par dire au wrapper de timeout qu'elle s'est arrêtée. C'est seulement à ce moment-là que le wrapper de timeout te raise une TimeoutError. Voici le point clé. Depuis Python 3.8, CancelledError hérite directement de BaseException, et non de la classe Exception standard. Ce choix de design évite une erreur spécifique et catastrophique. Les devs wrap couramment les opérations réseau ou de fichiers dans des blocs try et except qui catch les classes Exception génériques pour éviter un crash. Si CancelledError était une Exception standard, ces blocs attraperaient le signal de cancellation. La task pourrait par exemple logger un warning, avaler le signal, et continuer à s'exécuter comme un zombie. En remontant CancelledError dans la hiérarchie vers BaseException, Python garantit que tes error handlers habituels n'intercepteront pas accidentellement une requête de cancellation. Alors, comment tu gères le state en toute sécurité quand une task est cancelled ? Tu te reposes sur la structure try et finally. Prends l'exemple d'un serveur web qui traite une requête HTTP entrante. L'utilisateur demande un énorme rapport, puis ferme la fenêtre de son navigateur. Le serveur détecte la déconnexion et cancel la task de la requête. Dans ton code, tu es en train de faire un await sur une longue requête de base de données. Cet await raise soudainement une CancelledError. Comme tu as placé ton interaction avec la base de données dans un bloc try, l'exécution saute instantanément à ton bloc finally. Tu utilises ce bloc finally pour faire un rollback propre de la transaction en attente et rendre la connexion de base de données au pool. Une fois que le bloc finally se termine, la CancelledError continue de remonter, ce qui termine la task avec succès. Parfois, un bloc finally ne suffit pas. Si tu dois absolument faire un cleanup asynchrone, comme envoyer une requête réseau à un microservice distant pour annoncer l'abandon, tu peux catch explicitement la CancelledError. Mais si tu fais ça, tu dois explicitement re-raise cette erreur exacte à la fin de ton bloc except. Oublier de la re-raise casse la mécanique interne d'asyncio. La task aura l'air de s'être terminée avec succès au lieu d'être cancelled, ce qui corrompt le state de ton application et casse la structured concurrency. La règle à retenir, c'est que la cancellation est une requête coopérative, pas une commande kill brutale, et elle repose entièrement sur le fait que les exceptions remontent sans être touchées. Si tu veux soutenir l'émission, tu peux chercher DevStoriesEU sur Patreon. C'est tout pour cet épisode. Merci de ton écoute, et continue de développer !
7

Céder le Contrôle avec Sleep

3m 14s

Comprenez le véritable but d'asyncio.sleep(0). Découvrez comment céder le contrôle empêche les boucles gourmandes en CPU d'affamer l'event loop et de geler l'application.

Télécharger
Salut, c'est Alex de DEV STORIES DOT EU. asyncio, épisode 7 sur 20. Parfois, le secret pour garder ton serveur réseau réactif, c'est de dire à tes tâches les plus lourdes de faire un sleep d'exactement zéro seconde. Si une fonction ne fait jamais de pause, toute ton application arrête d'écouter le monde extérieur. Pour corriger ça, tu rends le contrôle en utilisant sleep. Dans le framework asyncio, l'event loop exécute exactement une tâche à la fois. Elle repose entièrement sur le multitâche coopératif. Une tâche tourne en continu jusqu'à ce qu'elle rencontre un mot-clé await, qui sert de checkpoint pour rendre le contrôle de l'exécution à l'event loop. Si tu écris une fonction async qui contient une opération purement CPU-bound, tu crées un goulot d'étranglement. Pense au parsing d'un énorme payload JSON ou à la transformation de milliers de strings. Il n'y a pas de points d'await naturels dans une boucle de traitement de données standard. Comme la tâche ne fait jamais de yield, l'event loop reste bloquée. Toutes les requêtes réseau entrantes, les réponses de la base de données ou les health checks restent coincés dans une queue, affamés en attendant que ta boucle se termine. La façon native de résoudre ça, c'est de rendre manuellement le contrôle à l'event loop. Tu fais ça en utilisant un idiome spécifique : faire un await sur asyncio point sleep avec un argument de zéro. À première vue, faire un sleep de zéro seconde ressemble à une opération inutile. Pourquoi demander au système de ne pas attendre du tout ? Voici l'idée clé. Un sleep de zéro seconde n'a rien à voir avec le passage du temps. C'est un signal explicite pour l'event loop. Quand tu fais un await sur un sleep de zéro, la coroutine actuelle est immédiatement suspendue. L'event loop prend le relais, place ta tâche suspendue à la fin de la queue d'exécution, et vérifie si d'autres tâches planifiées sont prêtes à s'exécuter. Si un handler réseau en background attend d'accepter une connexion entrante, c'est son tour. Une fois que les autres tâches atteignent leurs propres points d'await ou se terminent, ta tâche initiale remonte à l'avant de la queue et reprend exactement là où elle s'était arrêtée. Appliquons ça à un scénario concret. Tu écris une fonction async pour traiter des millions d'enregistrements depuis un fichier JSON. Si tu fais tourner une boucle while d'une traite, ton serveur a l'air mort. À la place, tu introduis une variable compteur. À l'intérieur de la boucle, tu traites un enregistrement et tu incrémentes le compteur. Ensuite, tu ajoutes une condition simple. Si le compteur indique que cent itérations sont passées, tu fais un await sur asyncio point sleep zéro. Ça divise le calcul massif en chunks gérables. La boucle traite cent enregistrements, s'écarte pour laisser le serveur répondre aux pings ou accepter de nouvelles données, puis reprend le parsing des cent suivants. Le nombre d'itérations entre les yields est un paramètre que tu dois ajuster. Faire un yield à chaque itération ajoute trop d'overhead, parce que suspendre et reprendre une coroutine a un petit coût de calcul. Faire un yield toutes les dix mille itérations risque de bloquer l'event loop trop longtemps. Cent est un point de départ raisonnable pour laisser la boucle respirer. Forcer un sleep de zéro seconde est le moyen le plus simple de garder ton application coopérative, en t'assurant qu'une seule boucle lourde n'affame jamais le reste de ton système. Merci d'avoir écouté, et bon code à tous !
8

Synchronisation : Locks et Mutexes

3m 36s

Prévenez les race conditions dans le code async. Nous explorons asyncio.Lock, discutons de sa nature non thread-safe, et montrons pourquoi les locks de threading gèleront votre event loop.

Télécharger
Salut, c'est Alex de DEV STORIES DOT EU. asyncio, épisode 8 sur 20. Tu ajoutes un threading lock standard dans ton application async pour protéger une ressource partagée, et soudain, toute ton event loop freeze complètement. Le lock a fait son job, mais il a bloqué tout le reste du process. Pour résoudre ça sans bloquer la loop, on utilise la synchronisation : les Locks et les Mutexes. Un Lock asyncio, souvent appelé un mutex, garantit un accès exclusif à une ressource partagée entre les tasks async. D'abord, on doit dissiper une confusion courante. Tu ne peux pas utiliser un thread lock standard du module threading de Python dans une application async. Un threading lock opère au niveau du système d'exploitation. S'il ne peut pas acquérir le lock, il met tout le thread en pause. Comme asyncio fait tourner plusieurs tasks de manière coopérative sur un seul thread, bloquer ce thread signifie que l'event loop s'arrête. Plus aucune requête réseau ne part, plus aucun timer ne tourne. Tout freeze. Un lock asyncio résout ça en étant task-safe, et non thread-safe. Quand une task asyncio essaie d'acquérir un mutex verrouillé, elle ne bloque pas le thread. À la place, elle se suspend et rend le contrôle à l'event loop. Ça permet à d'autres tasks indépendantes de continuer leur boulot pendant que la première task attend son tour. Prenons un scénario concret. Tu as une application avec des dizaines de tasks async qui font des appels d'API externes. Ton token OAuth expire. Deux tasks différentes remarquent que le token a expiré à la milliseconde près. Sans synchronisation, les deux tasks vont envoyer indépendamment une requête au serveur d'authentification pour refresh le token. Ce travail redondant peut déclencher des rate limits ou invalider immédiatement le premier token à cause de politiques de rotation strictes. Pour éviter cette race condition, tu crées un seul lock asyncio quand tu initialises ton application. Cet objet lock est passé ou partagé entre toutes tes tasks d'API. Maintenant, regarde le flow. La Task A et la Task B détectent toutes les deux le token expiré. La Task A atteint le bloc de synchronisation en premier et fait un await sur le lock. Elle l'acquiert avec succès. La Task B arrive une fraction de seconde plus tard et fait un await sur le même lock. Comme la Task A le détient, la Task B se met en veille, laissant l'event loop gérer d'autres trucs. Quand plusieurs tasks attendent le même lock, asyncio les met en file d'attente. Une fois que le lock est libéré, l'event loop réveille la première task de la file. La Task A demande le nouveau token de manière sécurisée, met à jour la variable partagée du token, et libère le lock. À ce moment-là, l'event loop réveille la Task B. La Task B acquiert enfin le lock. Cependant, avant de faire un appel réseau, la Task B vérifie à nouveau le token. Elle voit que le token est déjà valide, saute l'étape de refresh, libère le lock, et continue avec sa requête d'API principale. La façon la plus sûre d'implémenter cette logique, c'est d'utiliser un context manager asynchrone. Dans ton code, tu écris une instruction async with suivie de l'objet lock. Quand l'exécution entre dans ce bloc, elle attend un accès exclusif. Quand l'exécution sort du bloc, que ce soit normalement ou parce qu'une erreur a fait crasher la task, elle libère automatiquement le lock. Tu n'as pas besoin d'appeler manuellement les méthodes acquire ou release, ce qui élimine le risque de laisser accidentellement un lock engagé pour toujours. Voici le point clé. Un lock asyncio ne protège pas ton state des autres threads du système d'exploitation ; il protège ton state de tes propres tasks concurrentes qui se marchent dessus pendant qu'elles font un await sur d'autres opérations. Merci d'avoir écouté. J'espère que tu as appris un truc nouveau.
9

Coordonner l'État avec les Events

3m 52s

Apprenez à diffuser des signaux à plusieurs tâches en attente. Nous expliquons comment asyncio.Event et asyncio.Condition remplacent élégamment les boucles de polling inefficaces.

Télécharger
Salut, ici Alex de DEV STORIES DOT EU. asyncio, épisode 9 sur 20. Tu as cinquante tâches qui attendent qu'une base de données se connecte. Tu ne veux surtout pas qu'elles fassent du polling en boucle, en gaspillant des cycles CPU pendant qu'elles vérifient si la connexion est prête. Il te faut un seul signal broadcast qui leur dit à toutes de commencer à faire leurs requêtes exactement au même moment. C'est exactement ce que gère la coordination d'état avec les Events et les Conditions. Un Event asyncio gère un simple flag booléen interne. Il commence à false. Avant de regarder le flow, clarifions une confusion courante entre les Events et les Locks. Un Lock donne un accès exclusif à exactement une tâche à la fois, en bloquant les autres. Un Event fait l'inverse. Il notifie plusieurs tâches en attente simultanément, en les laissant toutes continuer en même temps. Repense à ce scénario de connexion à la base de données. Ta tâche en background travaille pour établir la connexion. Pendant ce temps, tes cinquante tâches worker arrivent à un point où elles ont besoin de la base de données. Chaque worker appelle la méthode wait sur ton objet Event partagé. Comme le flag interne est à false, les cinquante tâches se suspendent. Elles restent inactives. Finalement, la tâche en background réussit et appelle la méthode set sur l'Event. Le flag passe à true. Instantanément, les cinquante tâches worker suspendues se réveillent et reprennent leur exécution. Si tu as besoin de fermer la connexion plus tard, tu peux appeler la méthode clear sur l'Event. Le flag repasse à false, et tous les futurs appels à wait bloqueront à nouveau. Tu peux aussi vérifier le statut actuel du flag à tout moment en appelant la méthode is set, qui renvoie true ou false sans bloquer la tâche. Ça couvre les signaux broadcast simples. Parfois, un simple flag booléen ne suffit pas. Tu peux avoir plusieurs tâches qui doivent attendre qu'une ressource partagée atteigne un état complexe spécifique, et elles ont besoin d'un accès exclusif pour vérifier ou modifier cet état en toute sécurité. C'est là qu'intervient la Condition asyncio. Une Condition est construite autour d'un Lock sous-jacent. Pour faire quoi que ce soit avec une Condition, une tâche doit d'abord l'acquérir. Une fois acquise, la tâche vérifie l'état partagé. Si l'état n'est pas ce dont la tâche a besoin, la tâche appelle la méthode wait sur la Condition. Voici le point clé. Appeler wait sur une Condition fait deux choses en même temps : ça libère le Lock sous-jacent, ce qui permet à d'autres tâches d'accéder à l'état, et ça suspend la tâche en cours. Pendant que cette tâche est suspendue, une autre tâche peut acquérir la Condition, changer l'état partagé, puis appeler la méthode notify. La méthode notify prend un argument qui spécifie exactement combien de tâches en attente il faut réveiller, avec une valeur par défaut de un. Tu peux aussi appeler notify all pour réveiller tout le monde d'un coup. Quand une tâche suspendue se réveille, elle ne s'exécute pas immédiatement. Elle doit attendre de réacquérir le Lock sous-jacent avant que la méthode wait ne retourne. Parce qu'une autre tâche pourrait prendre le Lock et changer l'état avant que la tâche réveillée n'ait son tour, l'appel à wait est presque toujours placé dans une boucle while qui vérifie en continu l'état désiré. Une fois qu'elle a récupéré le Lock et que l'état est correct, elle peut continuer en toute sécurité et finalement libérer la Condition. Au moment de choisir entre les deux, rappelle-toi qu'un Event est un simple broadcast qui dit aux tâches qu'une action ponctuelle s'est produite, alors qu'une Condition permet aux tâches d'attendre en toute sécurité un changement d'état complexe sans faire du polling constant sur une ressource verrouillée. Merci d'avoir passé quelques minutes avec moi. À la prochaine, et prends soin de toi.
10

Limiter la Concurrence avec les Semaphores

3m 35s

Protégez les ressources fragiles et évitez les bannissements liés au rate-limiting. Découvrez comment asyncio.Semaphore limite l'exécution concurrente sans bloquer votre architecture.

Télécharger
Salut, ici Alex de DEV STORIES DOT EU. asyncio, épisode 10 sur 20. Envoyer dix mille requêtes asynchrones à une API tierce fragile est un moyen infaillible de faire bannir définitivement ton adresse IP. Ton code fonctionne parfaitement, mais le serveur distant s'effondre sous le poids de ce pic de trafic soudain. Pour protéger les services externes et ton propre accès, tu dois throttler ton application. Ce bouclier, c'est de limiter la concurrency avec des Semaphores. Ça aide de clarifier tout de suite une idée reçue. Un Semaphore n'est pas un rate limiter. Il ne plafonne pas le nombre de requêtes que ton programme fait par seconde. À la place, il limite les opérations concurrentes. Il contrôle strictement combien de tasks peuvent exécuter un bloc spécifique d'opérations réseau ou de fichiers exactement au même moment. Si une task termine son appel API en dix millisecondes, ce slot se libère immédiatement pour la task suivante dans la file. Tu pourrais quand même traiter des centaines d'opérations par seconde, à condition qu'il n'y en ait pas plus que la limite autorisée in flight simultanément. Un Semaphore asyncio gère un simple compteur interne. Quand tu crées l'objet Semaphore, tu fournis une valeur initiale. Prenons le scénario où tu limites les requêtes HTTP sortantes vers une API externe fragile à exactement dix connexions concurrentes. Tu initialises ton Semaphore avec une valeur de dix. Avant qu'une task asynchrone ne fasse une requête réseau, elle doit acquire le Semaphore. Cette action diminue le compteur interne de un. Quand la requête réseau se termine, la task release le Semaphore, ce qui remonte le compteur de un. Voici le point clé. Si dix tasks ont déjà acquire le Semaphore, le compteur est à zéro. Quand la onzième task essaie de l'acquire, cette task est suspendue. La méthode acquire bloque la progression jusqu'à ce qu'une des dix premières tasks se termine et release son accès. Ce simple verrou numérique garantit que tu ne dépasses jamais ta hard limit de dix connexions actives. En pratique, tu ne devrais presque jamais appeler les méthodes acquire et release manuellement. À la place, tu utilises le Semaphore comme un context manager asynchrone. En wrappant ta requête HTTP dans un statement with asynchrone, Python garantit que le Semaphore est release à la sortie du bloc de code. Ce release se produit même si l'API fait un timeout, coupe la connexion, ou lève une exception non gérée. Si tu tentes des release manuels et qu'une erreur saute ton appel à release, ce slot de concurrency est perdu pour toujours. Si tu perds les dix slots à cause d'erreurs réseau passagères, ton programme entier fait un deadlock silencieusement. Il y a un danger subtil avec le Semaphore standard. Si une erreur de logique dans ton code pousse une task à release le Semaphore plus de fois qu'elle ne l'a acquire, le compteur interne va augmenter au-delà de ta limite initiale de dix. Soudain, ton bouclier de concurrency est brisé, et tu envoies sans le savoir douze ou quinze requêtes simultanées. Pour éviter ça, tu devrais utiliser un Bounded Semaphore asyncio. Un Bounded Semaphore se comporte exactement comme un Semaphore standard, mais il garde une trace de la valeur initiale que tu lui as donnée. Si une task rebelle essaie de release le Semaphore au-delà de cette limite de départ, le Bounded Semaphore lève immédiatement une ValueError. Il crashe tôt et bruyamment au lieu de surcharger silencieusement l'API externe. Utilise toujours un Bounded Semaphore par défaut, à moins que tu aies une raison architecturale très spécifique de gonfler dynamiquement tes limites de concurrency. Les Bounded Semaphores attrapent les erreurs logiques de release au moment où elles se produisent, gardant tes limites de connexion API strictes et tes systèmes fonctionnant de manière prévisible. C'est tout pour cet épisode. Merci de ton écoute, et continue de coder !
11

Workflows Producteur-Consommateur

3m 20s

Découplez les producteurs rapides des consommateurs lents en toute sécurité. Explorez asyncio.Queue, la signalisation de fin de tâche et les nouvelles mécaniques de shutdown pour les Queues.

Télécharger
Salut, c'est Alex de DEV STORIES DOT EU. asyncio, épisode 11 sur 20. Tu as un serveur web async qui gère des milliers de requêtes par seconde, et pour chaque requête, tu dois écrire un log sur le disque. Si ton serveur attend la fin de cette écriture sur le disque avant de répondre, tes performances s'effondrent. La méthode la plus fiable pour découpler les producteurs rapides des consommateurs lents en Python async est directement intégrée à la bibliothèque standard. Aujourd'hui, on va regarder les workflows producteur-consommateur en utilisant les queues asyncio. Certains développeurs qui viennent de la programmation multi-thread pensent qu'ils doivent encapsuler cette queue dans des locks pour éviter les race conditions. Tu n'as pas besoin de le faire. La queue asyncio est conçue spécifiquement pour des tâches concurrentes qui tournent sur une seule event loop. Elle est intrinsèquement safe pour ces tâches. Garde les queues thread-safe du module queue standard pour le threading ; utilise la version asyncio pour l'async. Imagine la queue comme un pipe. D'un côté, tu as des producteurs qui poussent des items dedans. De l'autre côté, tu as des consommateurs qui récupèrent ces items. Reprenons ce scénario de logging. Ton request handler web est le producteur. Il reçoit une requête entrante, formate un événement de log, et appelle la méthode async put sur la queue. Si tu définis une taille maximale quand tu crées la queue, tu obtiens une backpressure automatique. Quand la queue est pleine, faire un await sur la méthode put met le producteur en pause jusqu'à ce que de l'espace se libère. Ça évite qu'un pic de trafic massif n'épuise la mémoire de ton système. De l'autre côté du pipe, tu as une background task séparée qui agit comme consommateur. Cette tâche tourne dans une boucle continue. Elle appelle la méthode async get sur la queue. Si la queue est vide, le consommateur se met en veille en toute sécurité. L'event loop le réveille au moment exact où un producteur dépose un nouvel événement de log dans le pipe. Le consommateur prend l'événement, l'écrit sur le disque, puis signale que ce job spécifique est terminé en appelant une méthode nommée task done. Gérer ce flow pendant le teardown de l'application est critique. Si tu dois éteindre ton serveur web proprement, tu veux t'assurer que tous les événements de log dans la queue soient bien écrits sur le disque. La queue possède une méthode appelée join. Quand tu fais un await sur join, ton programme se bloque jusqu'à ce que le nombre d'appels à task done corresponde exactement au nombre d'items initialement mis dans la queue. Ça garantit que chaque élément de travail a été entièrement traité. Voici le point clé. Python 3.13 a introduit une nouvelle méthode de queue appelée shutdown. Avant, pour arrêter proprement une boucle producteur-consommateur, il fallait passer des valeurs sentinelles spéciales, comme injecter un objet None dans la queue, juste pour dire au consommateur de quitter sa boucle. Maintenant, tu peux simplement appeler shutdown. Quand tu fais ça, toute tâche actuellement bloquée en attendant de faire un put ou un get sur un item se prend immédiatement une exception QueueShutDown. Tu attrapes cette exception dans tes worker tasks, tu nettoies tes ressources, et tu quittes proprement sans aucune logique de sentinelle fragile. Quand tu conçois un système asyncio, n'oublie pas que les queues ne sont pas juste des structures de données ; ce sont des mécanismes de flow control qui gèrent nativement la backpressure, gardant ton empreinte mémoire stable même quand les producteurs dépassent largement les consommateurs. C'est tout pour cette fois. Merci d'avoir écouté, et continue à développer !
12

Réseau de Haut Niveau avec les Streams

3m 47s

Plongez dans les IO Streams de haut niveau. Nous discutons de StreamReader, StreamWriter, et pourquoi omettre await writer.drain() peut détruire silencieusement la mémoire de votre serveur.

Télécharger
Salut, c'est Alex de DEV STORIES DOT EU. asyncio, épisode 12 sur 20. Tu envoies des données via une connexion réseau, et ta loop a l'air de marcher parfaitement. Mais en coulisses, ton application consomme silencieusement des gigaoctets de mémoire jusqu'à ce que le système la kill. Le problème vient généralement d'une seule ligne de code manquante qui gère le flow control. C'est pour ça qu'aujourd'hui, on va s'intéresser au networking de haut niveau avec les streams. Asyncio fournit une API de haut niveau pour bosser avec des connexions réseau sans toucher aux raw sockets ou aux protocoles de transport bas niveau. Pour établir une connexion TCP, tu utilises une fonction top-level appelée open_connection. Tu lui passes une string pour le host et un integer pour le port. Elle retourne immédiatement un tuple de deux objets : un StreamReader et un StreamWriter. Si tu codes un serveur plutôt qu'un client, tu utilises start_server. Tu fournis un callback, un host et un port. À chaque fois qu'un nouveau client se connecte, asyncio déclenche ton callback, en lui passant un reader et un writer dédiés pour cette connexion client spécifique. Le StreamReader, c'est ton interface pour recevoir des données. Il fournit des méthodes asynchrones pour récupérer des bytes sur le réseau. Tu peux lire un nombre maximum spécifique de bytes en utilisant la méthode read. Si tu parses des protocoles basés sur des lignes, tu peux lire jusqu'à un séparateur spécifique, comme un newline, avec la méthode readuntil. Si ton protocole nécessite un header de taille fixe, tu peux utiliser readexactly, qui va attendre jusqu'à ce que ce nombre exact de bytes arrive. Comme toutes ces opérations dépendent du trafic réseau et de la latence, elles mettent la coroutine en pause, ce qui veut dire que tu dois les await. Maintenant, la deuxième pièce du puzzle, c'est le StreamWriter. Cet objet gère l'envoi des données vers l'extérieur. Tu utilises la méthode write pour pousser des bytes dans le stream. Et voici le point crucial. La méthode write est une fonction classique, pas une fonction asynchrone. Tu ne l'await pas. Quand tu appelles write, tu ne mets pas instantanément les données sur le réseau. Tu places simplement les données dans un buffer interne d'asyncio. L'event loop sous-jacente essaie de flush ce buffer sur le réseau en arrière-plan. C'est avec ce buffer que les devs rencontrent des problèmes. Imagine un client TCP qui envoie un payload de fichier massif à un serveur lent. Si tu mets ton appel à write dans une loop serrée qui lit des chunks depuis un disque local, Python va lire le fichier beaucoup plus vite que le réseau ne peut le transmettre. Comme write ne bloque pas ton code, ta loop continue de tourner. Le buffer interne absorbe tout le fichier, et consomme toute la mémoire système disponible. C'est là qu'intervient la backpressure. Pour gérer le flow control, tu dois associer tes appels à write avec la méthode drain. La méthode drain est asynchrone, ce qui veut dire que tu dois l'await. Quand tu await drain, tu dis à l'event loop de mettre ta coroutine en pause si le buffer interne a dépassé sa high-water mark. Ton code attend que le process en arrière-plan pousse assez de données sur le réseau pour réduire le buffer à une taille sûre. Le réseau a le temps de rattraper son retard, le buffer se vide, et ton utilisation de la mémoire reste stable. Quand tu as fini d'envoyer ton fichier, tu appelles la méthode close sur le writer. Tout comme write, close n'est pas une fonction async. Pour t'assurer que la connexion se ferme proprement et que tous les derniers bytes soient flushed avant que ton programme ne passe à la suite, tu fais suivre ça par un await sur la méthode wait_closed. Le StreamWriter donne l'impression que l'écriture sur le réseau est instantanée, mais les lois de la physique s'appliquent toujours. Fais toujours un await sur drain après un write pour t'assurer que ton application respecte la vitesse réelle de la connexion réseau. Merci de m'avoir écouté, et happy coding tout le monde !
13

Créer des Serveurs Async

3m 19s

Construisez des serveurs réseau hautement concurrents. Apprenez comment asyncio.start_server abstrait les connexions clientes, en générant une tâche isolée pour chaque pair.

Télécharger
Salut, c'est Alex de DEV STORIES DOT EU. asyncio, épisode 13 sur 20. Créer un serveur TCP hautement concurrent en Python implique généralement de se battre avec des thread pools ou des configurations complexes d'event loop. En fait, tu peux gérer des milliers de connexions en moins de dix lignes de code. C'est exactement ce qu'on va voir aujourd'hui en créant des serveurs asynchrones avec les streams asyncio. La base d'un serveur réseau dans asyncio, c'est une fonction qui s'appelle start_server. Tu lui passes trois choses : un callback, une adresse IP et un port. Quand tu fais un await sur start_server, elle se bind à cette adresse et commence à écouter les connexions TCP entrantes sur l'interface réseau que tu as spécifiée. Les développeurs pensent souvent qu'ils doivent intercepter manuellement ces connexions entrantes et écrire du boilerplate pour les dispatcher vers des worker threads ou des background tasks personnalisées. C'est complètement inutile. Le framework gère la concurrence pour toi. À chaque fois qu'un nouveau client se connecte à ton port, start_server spawn automatiquement une toute nouvelle task asyncio dédiée entièrement à ce client spécifique. Imagine que tu crées un simple serveur de chat. Quand ton premier utilisateur se connecte, start_server déclenche ton callback et lui passe deux objets : un stream reader et un stream writer. Si cinquante autres utilisateurs se connectent en même temps, cinquante tasks séparées se lancent instantanément pour exécuter exactement ce même callback. Chaque task reçoit sa propre paire isolée de reader et writer. À l'intérieur de ton callback, tu écris la logique comme si tu ne parlais qu'à une seule personne à la fois. Tu utilises l'objet reader pour écouter les messages entrants. Tu fais un await sur la méthode read du reader, en spécifiant un nombre maximum de bytes que tu veux accepter, comme cent bytes. Le reader te donne des bytes bruts venant du réseau, que tu décodes en une string de texte standard. Pour répondre au client, tu inverses le processus. Tu encodes ta string de réponse en bytes et tu la passes directement à l'objet writer. Voici le point essentiel. Passer des données au writer n'est pas une opération asynchrone, mais s'assurer que ces données quittent réellement la machine physique, ça l'est. Après avoir donné les données au writer, tu dois faire un await sur la méthode drain du writer. Le drain met en pause ta task client actuelle jusqu'à ce que le buffer réseau du système d'exploitation ait assez d'espace libre pour pousser les bytes sur le réseau. Cette étape est critique car elle empêche ton serveur de consommer toute la mémoire disponible si un client a une connexion réseau lente. Quand la conversation se termine, ou si le client se déconnecte, tu dis au writer de se fermer. Tu fais ensuite un await sur la méthode wait_closed du writer pour t'assurer que tous les derniers bytes sont transmis et que la socket sous-jacente se ferme proprement. De retour dans ta fonction de configuration principale, start_server a renvoyé un objet server. Par défaut, le server arrête d'écouter si le script Python principal atteint la fin de ses instructions. Pour garder ton serveur de chat ouvert indéfiniment, tu prends cet objet server et tu fais un await sur sa méthode serve_forever. Ça bloque la task asyncio principale dans une boucle infinie, qui accepte silencieusement de nouvelles connexions et lance de nouvelles tasks client en background. La vraie puissance de ce design, c'est qu'il abstrait la complexité du réseau. Tu écris du code simple et séquentiel pour une seule connexion isolée, et l'event loop le scale automatiquement sur des tasks concurrentes. Si tu veux soutenir l'émission, tu peux chercher DevStoriesEU sur Patreon. C'est tout pour cet épisode. Merci d'avoir écouté, et continue de développer !
14

Subprocesses Non-bloquants

3m 18s

Exécutez des commandes shell de manière asynchrone. Découvrez pourquoi l'utilisation du module subprocess standard arrête l'event loop, et comment asyncio.create_subprocess_exec corrige cela.

Télécharger
Salut, ici Alex de DEV STORIES DOT EU. asyncio, épisode 14 sur 20. Tu crées une API web asynchrone, tu déclenches une commande système standard dans un endpoint, et soudain, toutes les autres tâches concurrentes se paralysent instantanément. Plus rien ne bouge tant que cette commande système n'est pas terminée. Le coupable est le module subprocess standard de Python, et pour résoudre ça, il faut utiliser des subprocess non bloquants. Appeler une fonction comme subprocess point run exécute une commande du système d'exploitation et attend qu'elle se termine. Dans une application Python asynchrone, l'event loop tourne sur un seul thread. Quand tu bloques ce thread en attendant le système d'exploitation, l'event loop s'arrête. Toutes les autres requêtes concurrentes vers ton API restent gelées. Pour corriger ça, asyncio fournit ses propres fonctions de subprocess conçues spécifiquement pour l'event loop. L'outil principal est asyncio point create subprocess exec. Voici l'idée clé. Cette fonction n'exécute pas la commande directement en Python. Elle demande au système d'exploitation de créer un processus enfant, mais au lieu de bloquer en attendant le résultat, elle rend immédiatement le contrôle à l'event loop. Ton API gère d'autres requêtes pendant que le programme externe tourne. Prends le scénario d'une API web qui convertit des fichiers vidéo avec FFmpeg. Tu veux déclencher la conversion et streamer les logs de sortie vers l'utilisateur en temps réel. Dans ton endpoint async, tu appelles create subprocess exec. Tu passes le nom du programme, FFmpeg, suivi de ses arguments. Pour capturer les logs, tu dis à la fonction de rediriger le standard output et le standard error vers des pipes asyncio. La fonction retourne un objet Process asyncio. Cet objet représente la commande de l'OS en cours d'exécution et te donne des hooks asynchrones pour interagir avec elle. Parce que tu as redirigé les sorties vers des pipes, l'objet Process les expose comme des stream readers asynchrones. Tu lis les logs de FFmpeg en itérant sur le stream de standard error de manière asynchrone, puisque FFmpeg y écrit généralement ses logs. Pour chaque ligne que le processus externe produit, ta loop async se réveille, lit la ligne, et la streame en retour à l'utilisateur web. En attendant la ligne suivante, l'event loop Python retourne directement servir d'autres utilisateurs. Tu obtiens un streaming de logs en temps réel sans geler le serveur. Si tu n'as pas besoin de streamer l'output ligne par ligne, l'objet Process fournit aussi une méthode communicate async. Tu fais un await sur communicate pour envoyer des données au standard input et lire toutes les données de standard output et de standard error d'un seul coup. Ça garde la loop libre jusqu'à ce que le processus externe se termine complètement et retourne les données. Si tu as géré les streams manuellement comme dans l'exemple FFmpeg, tu fais plutôt un await sur la méthode wait de l'objet Process pour attendre que le processus se termine et récupérer son exit code. L'event loop se fiche de savoir si c'est le système d'exploitation qui fait le vrai calcul ; si ton code Python attend de manière synchrone que l'OS réponde, toute ton application asynchrone est complètement morte. C'est tout pour cet épisode. Merci pour ton écoute, et continue à développer !
15

Futures : La Passerelle de Bas Niveau

3m 36s

Déballez les fondations des instructions await. Nous examinons asyncio.Future, son rôle en tant que résultat éventuel, et comment il fait le pont entre le code hérité basé sur les callbacks et la syntaxe moderne.

Télécharger
Salut, c'est Alex de DEV STORIES DOT EU. asyncio, épisode 15 sur 20. Tu écris du code asynchrone moderne et propre, mais tu finis par devoir interagir avec une vieille library têtue qui repose entièrement sur des callbacks. Tu ne peux pas faire un await directement sur un callback, ce qui casse tout ton flux asynchrone. Le mécanisme qui fait le pont entre ces deux mondes, ce sont les Futures : le pont de bas niveau. Mettons les choses au clair tout de suite. On confond souvent les Tasks et les Futures. Une Task est une subclass spécifique d'un Future. Une Task encapsule une coroutine et la planifie activement sur l'event loop, en pilotant son exécution étape par étape. Un Future, lui, n'exécute rien. Il n'a pas de logique d'exécution propre. C'est simplement un conteneur d'état. C'est une primitive de bas niveau qui représente le résultat final d'une opération asynchrone. Quand tu écris du Python moderne, tu n'instancies quasiment jamais un Future directement. C'est l'event loop qui les crée sous le capot. Mais quand tu as besoin de wrapper du code legacy basé sur des callbacks, tu dois les construire manuellement. Imagine un scénario où tu utilises une vieille library de protocole réseau. Elle a une méthode request qui prend une adresse réseau, un callback de succès et un callback d'erreur. Tu veux que ta fonction async moderne fasse simplement un await sur cette request. Voici comment tu fais le pont. Dans ta fonction async, tu récupères l'event loop en cours d'exécution et tu lui demandes de créer un nouvel objet Future. À ce moment précis, le Future est dans un état pending. Il est vide et en attente. Ensuite, tu écris une petite fonction de callback de succès. Quand elle est déclenchée, cette fonction prend les données entrantes et appelle la méthode set result sur ton Future. Tu écris aussi un callback d'erreur qui appelle la méthode set exception sur le même Future. Tu passes ces deux fonctions à la méthode request legacy et tu lances l'appel réseau. Enfin, tu fais un await sur le Future. Voici le point clé. Faire un await sur un Future en état pending met en pause la coroutine actuelle. Ça rend le contrôle à l'event loop, ce qui permet à d'autres tasks de s'exécuter. Ton code reste figé à cette instruction await. Pendant ce temps, le client legacy fait ses entrées et sorties réseau en arrière-plan. Quand les données arrivent, le client legacy déclenche ton callback de succès. Ton callback appelle set result sur le Future. Le Future passe immédiatement de l'état pending à l'état finished. L'event loop remarque ce changement d'état. Elle réveille la coroutine qui attendait ce Future, dépaquette le résultat stocké, et ta fonction async reprend son exécution exactement comme si elle avait fait un await sur une coroutine native. Si l'appel réseau a échoué, ton callback d'erreur définit une exception sur le Future à la place. Quand l'event loop réveille la coroutine, elle lève exactement cette exception à la ligne du await. Un Future a des règles strictes concernant son état. Il ne peut sortir de l'état pending qu'une seule fois. Si un callback essaie d'appeler set result sur un Future qui est déjà finished, Python lève une Invalid State Error. Tu peux aussi annuler un Future manuellement. Si tu le fais, il passe dans un état cancelled, et toute coroutine qui fait un await dessus reçoit immédiatement une asyncio Cancelled Error. Les Futures fournissent la glue structurelle nécessaire entre les callbacks orientés événements et les instructions await qui ont l'air procédurales. Comprendre que chaque instruction await met finalement l'exécution en pause jusqu'à ce qu'un Future de bas niveau soit marqué comme finished, te donne une clarté totale sur la façon dont le Python asynchrone fonctionne réellement sous le capot. C'est tout pour cet épisode. Merci de ton écoute, et continue à développer !
16

Transports et Protocols

3m 51s

Passez sous le capot pour voir comment asyncio communique avec l'OS. Comprenez la relation 1:1 pilotée par les callbacks entre les Transports (comment les octets se déplacent) et les Protocols (ce que signifient les octets).

Télécharger
Salut, c'est Alex de DEV STORIES DOT EU. asyncio, épisode 16 sur 20. Quand tu utilises des streams asyncio de haut niveau, ton code a l'air propre, séquentiel, et tu peux utiliser await en toute sécurité. Mais sous ces coroutines sympas se cache un moteur hyper optimisé, callback-driven, qui gère les appels système un peu chaotiques. Pour comprendre comment ton application Python communique vraiment avec le réseau, tu dois t'intéresser aux Transports et aux Protocols. Ces deux abstractions sont la base du networking asyncio. Elles fonctionnent toujours par paire. Le protocol gère la logique applicative, en décidant quels bytes envoyer et comment interpréter les données entrantes. Le transport gère la mécanique. Il se fiche complètement de ce que signifient tes données ou de la façon dont elles sont formatées. Son seul boulot, c'est de trouver comment pousser ces bytes sur le réseau. Aujourd'hui, on se concentre à fond sur la couche transport. Imagine ce qui se passe quand tu écris directement sur une socket TCP non-blocking. Tu dois demander au système d'exploitation si la socket est prête. Tu dois gérer les écritures partielles si le buffer réseau est plein. Tu dois tracker quels bytes sont vraiment partis et lesquels doivent être renvoyés plus tard. Un transport asyncio cache toute cette complexité. Il agit comme un wrapper opaque autour de la raw socket et des appels système sous-jacents. En général, tu n'instancies jamais un transport toi-même. À la place, tu appelles une méthode de l'event loop pour créer une connexion réseau. L'event loop configure la socket, crée le transport, le lie à ton protocol, et te renvoie la paire. Voici le truc important à retenir. Une fois cette connexion établie, le transport prend le relais sur le buffering des inputs et des outputs. Quand ton protocol veut envoyer un message, il passe simplement un chunk de bytes à la méthode write du transport. Le transport ne bloque pas ton code pour attendre le réseau. Il place immédiatement ces bytes dans son propre buffer interne. Le transport bosse ensuite avec l'event loop en background, en lançant les appels de socket non-blocking au système d'exploitation. Si le système ne peut prendre que la moitié des bytes pour le moment, le transport garde le reste et réessaie à la prochaine itération de la loop. Ton application n'a jamais besoin de micro-manager cette queue. Le flow control est directement intégré à ce mécanisme. Si tu écris des données plus vite que le réseau ne peut les envoyer, le buffer interne du transport va commencer à se remplir. Une fois qu'il atteint une limite définie, le transport déclenche un callback spécifique sur ton protocol pour mettre l'écriture en pause. Quand le buffer se vide enfin, il déclenche un autre callback pour reprendre. Côté réception, le transport écoute l'event loop. Quand le système d'exploitation signale que des bytes entrants sont arrivés, le transport les récupère de la socket et les envoie directement dans le protocol via un callback. À ce bas niveau, tout est purement callback-driven. Il n'y a pas d'awaitables ici. Les transports fournissent aussi des méthodes standardisées pour gérer le lifecycle de la connexion. Tu peux fermer un transport proprement, ce qui lui dit de finir d'envoyer toutes les données en buffer avant de fermer la socket en toute sécurité. Si ça se passe mal, tu peux appeler la méthode abort pour couper la connexion immédiatement, en jetant tout ce qui reste dans la queue. Et si ton protocol a besoin de savoir à qui il parle, le transport fournit une méthode pour demander des infos supplémentaires, ce qui te permet de regarder à travers l'abstraction et de récupérer l'adresse IP de la socket sous-jacente ou les détails du peer. L'abstraction du transport est ce qui permet à ton code asyncio de rester purement concentré sur la logique des données. Les transports isolent ton application de la mécanique chaotique des I/O non-blocking ; ils prennent les raw bytes de ton protocol et gèrent silencieusement le buffering, les retries, et les appels de socket du système d'exploitation nécessaires pour les faire transiter sur le réseau. C'est tout pour aujourd'hui. Merci d'avoir écouté, et continue de coder !
17

Le Threading dans un Monde Async

3m 16s

Faites le pont entre les mondes synchrone et asynchrone. Apprenez comment décharger du code bloquant lourd en toute sécurité en utilisant des executors et des callbacks thread-safe sans bloquer l'event loop.

Télécharger
Salut, c'est Alex de DEV STORIES DOT EU. asyncio, épisode 17 sur 20. Tu ajoutes un background thread standard dans ton web server async pour gérer une tâche lente, et soudain, ton application commence à faire des deadlocks ou à lancer des erreurs d'état cryptiques. Mélanger des threads standards avec une event loop async est la recette pour un désastre, à moins d'utiliser les ponts thread-safe prévus pour ça. Aujourd'hui, on parle du Threading dans un monde Async. La règle principale d'asyncio, c'est que l'event loop tourne dans un seul thread. À cause de ça, presque tous les objets asyncio ne sont pas thread-safe. Une erreur classique, c'est de lancer un background thread standard, de faire un peu de travail, puis d'essayer de résoudre une future async ou de planifier un callback directement depuis ce thread. Si tu touches à un objet asyncio depuis un autre thread que celui qui fait tourner l'event loop, tu vas corrompre l'état de la loop. Pour envoyer un message depuis un background thread vers ton event loop, tu dois utiliser call soon threadsafe. C'est une méthode sur la loop elle-même. Tu lui passes le callback que tu veux lancer et les arguments. Au lieu de l'exécuter tout de suite, ton background thread place ce callback dans une queue interne sécurisée. L'event loop principale vérifie cette queue et exécute ton callback en toute sécurité dans le main thread pendant son cycle normal. C'est la seule façon sûre pour un thread externe d'interagir avec l'event loop. Maintenant, imagine la situation inverse. Tu fais tourner ton event loop async, et tu as besoin d'exécuter un bout de code synchrone et bloquant. Un scénario classique, c'est de faire une requête sur un driver PostgreSQL lent et synchrone comme psycopg2. Si tu exécutes une requête de base de données de cinq secondes directement dans ton request handler async, tout ton web server s'arrête. L'event loop ne peut traiter aucun autre trafic réseau ni aucun timer tant que cette requête de base de données n'a pas retourné son résultat. Voici l'idée clé. Pour éviter que la loop ne freeze, tu pousses ce travail bloquant vers un thread séparé en utilisant run in executor. C'est une autre méthode de l'event loop. Tu lui passes un thread pool executor et ta fonction de base de données synchrone. La loop confie la fonction à un background thread dans le pool et retourne immédiatement un objet awaitable. Tu await cet objet. Pendant que ta requête de base de données tourne dans le background thread, ton event loop est totalement libre de mettre cette task spécifique en pause et d'aller gérer des centaines d'autres requêtes web. Une fois que le driver PostgreSQL retourne enfin les données, le thread pool renvoie le résultat en toute sécurité à l'event loop. Ton awaitable se résout, et ta fonction async d'origine reprend son exécution exactement là où elle s'était arrêtée, avec maintenant les résultats de la base de données. Tu as deux ponts à sens unique. Utilise call soon threadsafe pour pousser des événements depuis un worker thread vers ta loop async. Utilise run in executor pour sortir le travail synchrone bloquant de ta loop async et le pousser vers un worker thread. Ne laisse jamais un appel synchrone pirater ton event loop, et ne laisse jamais un background thread toucher directement à tes objets async. C'est tout pour aujourd'hui. Merci d'avoir écouté, et continue de coder !
18

Générateurs Async et Nettoyage

3m 32s

Évitez les fuites de ressources avec les générateurs async. Nous explorons pourquoi l'itération 'async for' peut laisser des connexions pendantes lorsqu'elle est interrompue, et comment aclosing() offre de la sécurité.

Télécharger
Salut, c'est Alex de DEV STORIES DOT EU. asyncio, épisode 18 sur 20. Tu te prends un timeout en récupérant des lignes dans une base de données. Ton code gère l'exception et continue, mais quelques jours plus tard, ton application plante parce que ton connection pool est complètement épuisé. Tu es sorti d'une async loop trop tôt, et ça a laissé silencieusement des connexions à la base de données ouvertes en background. La solution, c'est de maîtriser les Async Generators et le cleanup. Quand tu écris un générateur asynchrone pour faire un yield d'éléments au fil du temps, tu gères souvent des ressources. Prends l'exemple d'un curseur de base de données. Tu écris un générateur qui récupère une connexion, qui yield les lignes une par une, et qui utilise un bloc try-finally pour renvoyer cette connexion au pool quand le fetch est terminé. Si tu itères sur chaque ligne, le générateur se termine, atteint le bloc finally et fait son cleanup. Tout marche bien. Le danger apparaît quand tu ne consommes pas tout le générateur. Si ton itération est wrappée dans un timeout, ou si tu tombes simplement sur un break après avoir trouvé la ligne qu'il te faut, le générateur se met en pause. Il est suspendu au dernier yield. Il n'a pas atteint le bloc finally. Ta connexion à la base de données reste ouverte. Tu pourrais t'attendre à ce que le garbage collector de Python finisse par gérer ça. Dans du code synchrone, quand un générateur perd toutes ses références et passe au garbage collector, Python injecte une exception de sortie qui exécute les blocs finally. Mais le code asynchrone complique les choses. Le garbage collection est un processus synchrone. Quand le garbage collector finit par trouver ton async generator suspendu, il ne peut pas exécuter de manière fiable le code de teardown asynchrone. L'event loop est peut-être occupée, ou même fermée. Compter sur le garbage collector pour clean up un async generator entraîne un comportement imprévisible et des dangling resources. C'est ça le plus important. La documentation officielle de asyncio dit explicitement que tu ne dois jamais compter sur le garbage collection pour le cleanup des async generators. Tu dois les fermer manuellement. La standard library fournit un outil direct pour ça qui s'appelle aclosing, qu'on trouve dans le module contextlib. Il agit comme un context manager asynchrone. Son seul job est de garantir que la méthode aclose du générateur est appelée et awaitée au moment où tu as fini de l'utiliser. Au lieu de passer directement ton générateur dans une boucle async for, tu le wrap. D'abord, tu crées l'instance du générateur. Ensuite, tu la passes à une instruction async with aclosing. À l'intérieur de ce bloc de contexte, tu lances ta boucle async for. Quand tu structures ton code comme ça, une sortie prématurée déclenche le context manager. Si un timeout interrompt la boucle, le bloc async with attrape la sortie. Il await explicitement la méthode aclose sur le générateur. Ça injecte l'exception de sortie en toute sécurité dans le générateur suspendu pendant que tu tournes encore activement dans l'event loop. Ton bloc finally s'exécute immédiatement, en faisant un await sur les étapes de teardown nécessaires, et ta connexion à la base de données retourne tranquillement dans le pool. À chaque fois qu'un async generator récupère des connexions réseau, des file descriptors ou des verrous de base de données, wrap-le dans aclosing avant d'itérer pour garantir un cleanup déterministe, peu importe les timeouts ou les breaks prématurés. C'est tout pour aujourd'hui. Merci d'avoir écouté, et continue de builder !
19

Maîtriser le Mode Debug

2m 59s

Attrapez les bugs de concurrence instantanément. Apprenez à utiliser PYTHONASYNCIODEBUG pour profiler les callbacks lents, dénicher les coroutines sans await, et identifier les exceptions jamais récupérées.

Télécharger
Salut, c'est Alex de DEV STORIES DOT EU. asyncio, épisode 19 sur 20. Ton serveur en production subit des lag spikes mystérieux, et tes opérations en background avalent des erreurs de façon aléatoire sans laisser de trace. Le problème ne vient pas de la logique de ton application, mais de la façon dont l'asyncio standard masque les erreurs de concurrence pour économiser des performances. Maîtriser le Debug Mode est la solution pour exposer instantanément ces échecs. Le Debug Mode d'asyncio agit comme un strict mode pour l'event loop. Par défaut, asyncio privilégie la vitesse brute par rapport aux vérifications de sécurité au runtime. Ça veut dire que quand les choses tournent mal, elles échouent souvent silencieusement. Tu actives le Debug Mode globalement en mettant la variable d'environnement PYTHONASYNCIODEBUG à un, ou en lançant Python avec le flag tiret X dev. Tu peux aussi l'activer dynamiquement en appelant set debug true directement sur l'objet event loop. Prends le scénario du lag spike. Tu as un serveur web qui gère des milliers de requêtes concurrentes, et soudainement, un seul endpoint fait freezer l'application entière. Tu soupçonnes qu'une opération regex rebelle bloque le thread, mais le logging standard te dit seulement quand une requête commence ou se termine, pas ce qui a bloqué la loop entre les deux. Quand le Debug Mode est actif, l'event loop mesure le temps d'exécution de chaque callback. Si un callback bloque la loop pendant plus de cent millisecondes, asyncio log automatiquement un warning. Ce warning inclut le fichier exact et le numéro de ligne où le freeze s'est produit, te pointant directement vers cette recherche regex coûteuse. Ce seuil de cent millisecondes est la valeur par défaut, mais tu peux l'ajuster pour tes besoins spécifiques de latence en modifiant la propriété slow callback duration sur la loop. Le Debug Mode attrape aussi les échecs d'exécution silencieux. Une erreur fréquente dans le code async, c'est d'appeler une fonction coroutine mais d'oublier le mot-clé await. La fonction retourne un objet coroutine, mais la logique en elle-même ne s'exécute jamais. En exécution normale, cet objet est discrètement jeté. Le Debug Mode tracke ça. Quand le garbage collector nettoie une coroutine sans await, la debug loop l'intercepte et émet un resource warning, montrant exactement où la coroutine orpheline a été créée pour que tu puisses corriger l'invocation. Ce même filet de sécurité s'applique aux background tasks. Si une task asyncio crashe, l'exception est stockée à l'intérieur de l'objet task lui-même. Si ton code ne fait jamais de await explicite sur cette task ou ne récupère pas son résultat, l'exception disparaît tout simplement. Avec le Debug Mode activé, asyncio surveille le cycle de vie de chaque task. Si une task est détruite et que son exception interne n'a jamais été récupérée, l'event loop log bruyamment l'erreur avec la traceback montrant où la task a été initialement spawnée. Ces vérifications ajoutent de l'overhead, donc tu laisses généralement le Debug Mode désactivé dans les environnements de production normaux, en le gardant pour le développement local ou le troubleshooting ciblé. Voici l'idée clé. Activer le Debug Mode transfère le fardeau de trouver les bugs de concurrence silencieux depuis ton propre logging manuel directement sur l'event loop elle-même. Si tu aimes le podcast et que tu veux nous soutenir, cherche simplement DevStoriesEU sur Patreon. C'est tout pour celui-ci. Merci d'avoir écouté, et continue de build !
20

Extension et Custom Loops

3m 39s

Le grand final. Nous explorons l'intégration avancée et ce qu'il faut pour écrire une event loop personnalisée ou sous-classer BaseEventLoop pour des environnements spécialisés et à haute performance.

Télécharger
Salut, c'est Alex de DEV STORIES DOT EU. asyncio, épisode 20 sur 20. Tu te heurtes à un mur de performances avec ton code asynchrone, et le profiling pointe directement sur l'event loop elle-même. Tu ne peux pas réécrire la standard library, mais tu as besoin d'un contrôle plus bas niveau sur la façon exacte dont le système gère les sockets et les tasks. La réponse se trouve dans l'extension et les custom loops. L'event loop standard d'asyncio n'est pas une boîte noire hardcodée. C'est une interface extensible. Elle a été conçue depuis le début pour être totalement remplaçable par des libraries C hautes performances ou des implémentations Python spécialisées. La plupart des développeurs d'applications n'auront jamais besoin de créer une custom loop. Par contre, si tu es auteur de framework ou que tu buildes une loop optimisée comme uvloop, tu dois bypasser le comportement standard et t'intégrer directement aux primitives bas niveau de l'OS. Pour créer une custom event loop, tu commences par subclasser BaseEventLoop. Cette base class définit tout le contrat sur la façon dont les opérations asynchrones doivent se comporter. En héritant de celle-ci, tu obtiens la structure, mais tu peux overrider des méthodes spécifiques pour intercepter et redéfinir des opérations fondamentales. Prends l'exemple de la création de socket. Dans une application standard, tu demandes à asyncio d'ouvrir une connexion, et il utilise l'implémentation de socket Python par défaut. Mais dans une subclass de custom loop, tu peux overrider les méthodes de création réseau. Ça veut dire que quand l'application demande une connexion réseau, ta custom loop intercepte cet appel. Tu peux ensuite router cette requête vers du code C hautement optimisé, ou la lier directement à des features avancées du kernel que le Python standard n'expose pas. Le code de l'application ne change pas, mais la machinerie sous-jacente est entièrement à toi. Ce contrôle granulaire s'applique aussi à la gestion des tasks. Voici le point clé. L'event loop est responsable du tracking de chaque task asynchrone. Si tu regardes sous le capot de BaseEventLoop, tu trouveras une méthode interne appelée underscore register task. En overridant cette méthode spécifique, ta custom loop intercepte une task à la microseconde exacte où elle est créée. Pourquoi c'est important ? Si tu buildes un custom runtime, tu pourrais avoir besoin de tracker des diagnostics profonds, d'implémenter du memory pooling spécialisé pour les tasks, ou d'envoyer l'état des tasks directement à un service de monitoring custom. Overrider underscore register task te donne un hook garanti dans le cycle de vie de chaque coroutine avant même qu'elle ne commence à s'exécuter. Tu peux aussi overrider la méthode unregister correspondante pour gérer le cleanup exactement comme ton framework l'exige. Une fois ta class de custom loop créée, tu dois dire à Python de l'utiliser. Tu fais ça en créant une custom event loop policy. La policy est juste une factory qui dicte quelle implémentation de loop est créée quand un thread en demande une. Tu définis ta custom policy globalement. À partir de là, n'importe quelle fonction de la standard library qui demande une event loop recevra ta version custom et optimisée. La vraie puissance d'asyncio, ce n'est pas juste la syntaxe async et await. C'est le fait que tout le moteur d'exécution est une interface pluggable, prête à être swappée au moment où les performances standard limitent ton architecture. Comme ça conclut notre série, je t'encourage à lire la documentation officielle, à essayer d'étendre ces composants toi-même, ou à visiter devstories dot eu pour suggérer des sujets pour les futures séries. C'est tout pour cet épisode. Merci d'avoir écouté, et continue de builder !