Python 3.5 a introduit les E/S asynchrones comme alternative aux threads pour gérer la concurrence. L'avantage des E/S asynchrones et de l'implémentation asyncio dans Python est qu'en ne générant pas de threads de système d'exploitation gourmands en mémoire, le système utilise moins de ressources et est plus évolutif. De plus, dans asyncio, les points de planification sont clairement définis via la syntaxe await
, alors que dans la concurrence basée sur les threads, le GIL peut être libéré à des points de code imprévisibles. En conséquence, les systèmes de concurrence asyncio sont plus faciles à comprendre et à déboguer. Enfin, la tâche asyncio peut être annulée, ce qui n'est pas évident à faire lorsqu'on utilise des threads.
Cependant, afin de réellement bénéficier de ces avantages, il est important d'éviter de bloquer les appels dans les coroutines asynchrones. Les appels bloquants peuvent être des appels réseau, des appels du système de fichiers, des appels sleep
, etc. Ces appels bloquants sont nuisibles car, sous le capot, asyncio utilise une boucle d'événements à thread unique pour exécuter des coroutines simultanément. Ainsi, si vous effectuez un appel de blocage dans une coroutine, cela bloque toute la boucle d'événements et toutes les coroutines, affectant les performances globales de votre application.
Ce qui suit est un exemple d'appel bloquant qui empêche l'exécution simultanée du code :
<code class="language-python">import asyncio import datetime import time async def example(name): print(f"{datetime.datetime.now()}: {name} start") time.sleep(1) # time.sleep 是一个阻塞函数 print(f"{datetime.datetime.now()}: {name} stop") async def main(): await asyncio.gather(example("1"), example("2")) asyncio.run(main())</code>
Le résultat en cours d'exécution est similaire à :
<code>2025-01-07 18:50:15.327677: 1 start 2025-01-07 18:50:16.328330: 1 stop 2025-01-07 18:50:16.328404: 2 start 2025-01-07 18:50:17.333159: 2 stop</code>
Comme vous pouvez le constater, les deux coroutines ne fonctionnent pas simultanément.
Pour surmonter ce problème, vous devez utiliser un équivalent non bloquant ou différer l'exécution vers le pool de threads :
<code class="language-python">import asyncio import datetime import time async def example(name): print(f"{datetime.datetime.now()}: {name} start") await asyncio.sleep(1) # 将阻塞的 time.sleep 调用替换为非阻塞的 asyncio.sleep 协程 print(f"{datetime.datetime.now()}: {name} stop") async def main(): await asyncio.gather(example("1"), example("2")) asyncio.run(main())</code>
Le résultat en cours d'exécution est similaire à :
<code>2025-01-07 18:53:53.579738: 1 start 2025-01-07 18:53:53.579797: 2 start 2025-01-07 18:53:54.580463: 1 stop 2025-01-07 18:53:54.580572: 2 stop</code>
Ici, deux coroutines s'exécutent simultanément.
Maintenant le problème c'est qu'il n'est pas toujours évident d'identifier si une méthode bloque ou non. Surtout si la base de code est volumineuse ou utilise des bibliothèques tierces. Parfois, les appels bloquants sont effectués dans des parties profondes du code.
Par exemple, ce code bloque-t-il ?
<code class="language-python">import blockbuster from importlib.metadata import version async def get_version(): return version("blockbuster")</code>
Python charge-t-il les métadonnées du package en mémoire au démarrage ? Est-ce fait lorsque le module blockbuster
est chargé ? Ou quand on appelle version()
? Les résultats sont-ils mis en cache et les appels ultérieurs seront-ils non bloquants ? La bonne réponse se fait lors de l'appel de version()
, ce qui implique la lecture du fichier METADATA du package installé. Et les résultats ne sont pas mis en cache. Par conséquent, version()
est un appel bloquant et doit toujours être reporté au fil de discussion. Il est difficile de savoir ce fait sans fouiller dans le code de importlib
.
Une façon de détecter les appels bloquants consiste à activer le mode de débogage d'asyncio pour enregistrer les appels bloquants qui prennent trop de temps. Mais ce n'est pas l'approche la plus efficace, car de nombreux temps de blocage plus courts que le délai d'expiration du déclenchement nuiront toujours aux performances, et les temps de blocage en test/développement peuvent être différents de ceux en production. Par exemple, les appels à la base de données peuvent prendre plus de temps dans un environnement de production si la base de données doit récupérer une grande quantité de données.
C'est là qu'intervient BlockBuster ! Une fois activé, BlockBuster corrigera plusieurs méthodes de framework Python bloquantes qui généreront des erreurs si elles sont appelées depuis la boucle d'événements asyncio. Les méthodes de correction par défaut incluent les méthodes des modules os
, io
, time
, socket
et sqlite
. Pour une liste complète des méthodes détectées par BlockBuster, consultez le fichier readme du projet. Vous pouvez ensuite activer BlockBuster en mode test unitaire ou développement pour détecter les appels bloquants et les corriger. Si vous connaissez la géniale bibliothèque BlockHound pour la JVM, c'est le même principe, mais pour Python. BlockHound a été une grande source d'inspiration pour BlockBuster, grâce aux créateurs.
Voyons comment utiliser BlockBuster sur l'extrait de code de blocage ci-dessus.
Tout d'abord, nous devons installer le blockbuster
package
<code class="language-python">import asyncio import datetime import time async def example(name): print(f"{datetime.datetime.now()}: {name} start") time.sleep(1) # time.sleep 是一个阻塞函数 print(f"{datetime.datetime.now()}: {name} stop") async def main(): await asyncio.gather(example("1"), example("2")) asyncio.run(main())</code>
On peut ensuite utiliser le luminaire pytest et la méthode blockbuster_ctx()
pour activer le BlockBuster au début de chaque test et le désactiver lors du démontage.
<code>2025-01-07 18:50:15.327677: 1 start 2025-01-07 18:50:16.328330: 1 stop 2025-01-07 18:50:16.328404: 2 start 2025-01-07 18:50:17.333159: 2 stop</code>
Si vous exécutez ceci avec pytest, vous obtiendrez
<code class="language-python">import asyncio import datetime import time async def example(name): print(f"{datetime.datetime.now()}: {name} start") await asyncio.sleep(1) # 将阻塞的 time.sleep 调用替换为非阻塞的 asyncio.sleep 协程 print(f"{datetime.datetime.now()}: {name} stop") async def main(): await asyncio.gather(example("1"), example("2")) asyncio.run(main())</code>
Remarque : Généralement, dans un projet réel, le luminaire
blockbuster()
sera configuré dans un fichierconftest.py
.
Je pense que BlockBuster est très utile dans les projets asyncio. Cela m'a aidé à détecter de nombreux problèmes d'appels bloquants dans les projets sur lesquels j'ai travaillé. Mais ce n'est pas une panacée. En particulier, certaines bibliothèques tierces n'utilisent pas les méthodes du framework Python pour interagir avec le réseau ou le système de fichiers, mais encapsulent plutôt les bibliothèques C. Pour ces bibliothèques, vous pouvez ajouter des règles dans votre configuration de test pour déclencher des appels de blocage vers ces bibliothèques. BlockBuster est également open source : les contributions sont les bienvenues pour ajouter des règles pour vos bibliothèques préférées dans le projet principal. Si vous constatez des problèmes et des domaines à améliorer, j'aimerais recevoir vos commentaires dans le suivi des problèmes du projet.
Quelques liens :
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!