Module asyncio de Python

asyncioest un module de la bibliothèque standard Python, utilisé pour écrire du code d'opérations d'E/S asynchrones.

asyncio offre un moyen efficace de gérer des tâches concurrentes, particulièrement adapté aux opérations intensives en E/S telles que les requêtes réseau, la lecture et l'écriture de fichiers, etc.

En utilisantasyncio, vous pouvez traiter plusieurs tâches simultanément dans un seul thread sans utiliser plusieurs threads ou processus.

Pourquoi avons-nous besoin d'asyncio ?

Dans la programmation synchrone traditionnelle, lorsqu'une tâche doit attendre la fin d'une opération d'E/S (comme une requête réseau), le programme se bloque jusqu'à ce que l'opération soit terminée. Cela conduit à une faible efficacité, en particulier lorsqu'il s'agit de traiter un grand nombre d'opérations d'E/S.

asyncioEn introduisant le modèle de programmation asynchrone, le programme peut continuer à exécuter d'autres tâches pendant l'attente des opérations d'E/S, ce qui améliore la concurrence et l'efficacité du programme.

Imaginez que vous gérez un restaurant :

  • Mode synchrone (fonctions ordinaires) :Vous n'avez qu'un seul chef. Le client A commande un steak, et le chef commence à faire frire le steak (cela nécessite d'attendre 5 minutes). Pendant ces 5 minutes de cuisson du steak, le chef est complètement occupé et ne peut rien faire d'autre. Même si le client B veut seulement un verre d'eau, il doit attendre.
  • Mode asynchrone (asyncio) :Vous avez plusieurs chefs (en réalité toujours un seul, mais très intelligent). Après que le chef a commencé à faire frire le steak du client A, il se rend compte qu'il doit attendre. Il marque immédiatement le steak comme en attente, puis se retourne pour verser de l'eau au client B. Après avoir versé l'eau, il revient vérifier si le steak est presque prêt. S'il ne l'est pas encore, il peut aller traiter la commande du client C. De cette façon, pendant le temps d'attente des E/S (comme faire frire un steak, les requêtes réseau, la lecture et l'écriture de fichiers), le chef (CPU) travaille toujours efficacement.

asyncio est la bibliothèque standard que Python utilise pour mettre en œuvre ce mode de travail intelligent. Elle vous permet d'écrire du code concurrent mono-thread, particulièrement adapté aux scénarios intensifs en E/S tels que les robots d'exploration web, les serveurs web, les microservices, etc.

Son cœur est la boucle d'événements, les coroutines et les tâches.


Concepts fondamentaux d'asyncio

1. Coroutine

Une coroutine estasynciol'un des concepts fondamentaux. C'est une fonction spéciale qui peut se mettre en pause pendant l'exécution et reprendre plus tard. Les coroutines sont définies à l'aide duasync defmot-clé et, à travers leawaitmot-clé, met en pause l'exécution pour attendre qu'une opération asynchrone se termine.

Exemple

import asyncio

async def say_hello():
    print("Hello")
    await asyncio.sleep(1)
    print("World")

2. Boucle d'événements

La boucle d'événements estasynciole composant central, responsable de la planification et de l'exécution des coroutines. Elle vérifie continuellement s'il y a des tâches à exécuter et appelle les fonctions de rappel correspondantes une fois les tâches terminées.

Exemple

async def main():
    await say_hello()

asyncio.run(main())

3. Tâche

Une tâche est une enveloppe autour d'une coroutine, représentant une coroutine en cours d'exécution ou qui sera exécutée. Vous pouvez utiliser laasyncio.create_task()fonction pour créer une tâche et l'ajouter à la boucle d'événements.

Exemple

async def main():
    task = asyncio.create_task(say_hello())
    await task

4. Future

Futureest un objet représentant le résultat d’une opération asynchrone. Il est généralement utilisé dans les API de bas niveau pour représenter une opération qui n’est pas encore terminée. Vous pouvez utiliser leawaitmot-clé pour attendreFuturepour terminer.

Exemple

async def main():
    future = asyncio.Future()
    await future

Utilisation de base et exemples de code

Comprenons les concepts ci-dessus à travers un exemple classique d’accès simultané à plusieurs URL.

Supposons que nous devions récupérer le contenu de trois URL différentes. Avec une approche synchrone, les tâches s’exécutent séquentiellement et le temps total est la somme des trois temps de requête. Avecasyncio, nous pouvons envoyer ces trois requêtes en même temps, et le temps total est proche de celui de la requête la plus lente.

Version synchrone (pour comparaison)

Exemple

import time
import requests

def fetch_url(url):
    """Simuler une requête réseau chronophage (version synchrone)"""
    print(f"Début de récupération : {url}")
    time.sleep(2)  # Simuler 2 secondes de délai réseau
    print(f"Récupération terminée : {url}")
    return f"Données de {url}"

def main_sync():
    urls = ['https://example.com/1', 'https://example.com/2', 'https://example.com/3']
    results = []
    start = time.time()
   
    for url in urls:
        result = fetch_url(url)  # Doit attendre la fin du précédent avant de commencer le suivant
        results.append(result)
   
    end = time.time()
    print(f"Temps total de la version synchrone : {end - start:.2f} secondes")
    print(f"Résultats : {results}")

if __name__ == "__main__":
    main_sync()

Sortie attendue:

开始获取: https://example.com/1
完成获取: https://example.com/1
开始获取: https://example.com/2
完成获取: https://example.com/2
开始获取: https://example.com/3
完成获取: https://example.com/3
同步版本总耗时: 6.00 秒
结果: [‘来自 https://example.com/1 的数据‘, ‘来自 https://example.com/2 的数据‘, ‘来自 https://example.com/2 的数据‘]

Cela prend environ 6 secondes au total.

Version asynchrone (avec asyncio)

Nous devons utiliseraiohttpla bibliothèque pour remplacerrequestspour effectuer des requêtes HTTP asynchrones. Tout d'abord, installez-la :pip install aiohttp。

Exemple

import asyncio
import aiohttp
import time

async def fetch_url_async(session, url):
    """Simuler une requête réseau chronophage (version asynchrone)"""
    print(f"Début de la récupération asynchrone : {url}")
    # Remarque : Ici, nous utilisons la méthode get asynchrone d'aiohttp et attendons avec await
    async with session.get(url) as response:
        # Simuler que le traitement de la réponse prend aussi du temps
        await asyncio.sleep(2)  # Utiliser asyncio.sleep pour simuler l'attente d'E/S ; cela ne bloque pas le thread
        text = await response.text()
        print(f"Récupération asynchrone terminée : {url}")
        return f"Données de {url} (longueur : {len(text)})"

async def main_async():
    urls = ['https://httpbin.org/get', 'https://httpbin.org/delay/1', 'https://httpbin.org/headers']
   
    async with aiohttp.ClientSession() as session:  # Créer une session HTTP asynchrone
        # Créer une tâche (Task) pour chaque URL
        tasks = []
        for url in urls:
            # create_task ajoute la coroutine à la boucle d'événements et démarre immédiatement la planification
            task = asyncio.create_task(fetch_url_async(session, url))
            tasks.append(task)
       
        print("Toutes les tâches sont créées, démarrage de l'exécution concurrente...")
       
        # Utilisez asyncio.gather pour exécuter toutes les tâches simultanément et attendre qu'elles se terminent toutes
        # gather renvoie une liste de résultats, l'ordre correspondant à celui des tâches passées en entrée
        results = await asyncio.gather(*tasks)
       
        return results

if __name__ == "__main__":
    start = time.time()
    # asyncio.run() est un moyen pratique de démarrer la boucle d'événements et d'exécuter la coroutine de premier niveau
    final_results = asyncio.run(main_async())
    end = time.time()
   
    print(f"\n"Durée totale de la version asynchrone : {end - start:.2f} secondes")
    for res in final_results:
        print(res)

Sortie attendue:

所有任务已创建,开始并发执行...
开始异步获取: https://httpbin.org/get
开始异步获取: https://httpbin.org/delay/1
开始异步获取: https://httpbin.org/headers
(大约 2 秒后,所有请求几乎同时完成)
完成异步获取: https://httpbin.org/headers
完成异步获取: https://httpbin.org/get
完成异步获取: https://httpbin.org/delay/1

异步版本总耗时: 2.10 秒  # 注意!总耗时远小于 6 秒
来自 https://httpbin.org/get 的数据 (长度: 274)
来自 https://httpbin.org/delay/1 的数据 (长度: 392)
来自 https://httpbin.org/headers 的数据 (长度: 177)

Analyse de code:

  • async def: définit la fonction coroutinefetch_url_asyncetmain_async。
  • await: dansfetch_url_async, nousawait session.get()etawait response.text(), cela indique à la boucle d'événements : « Cette requête réseau prend du temps ; va d'abord exécuter d'autres tâches prêtes. »
  • asyncio.create_task(): enveloppe lafetch_url_asynccoroutine en uneTask, ce qui lui permet d'être planifiée par la boucle d'événements et d'atteindre la concurrence.
  • asyncio.gather(*tasks): une fonction très pratique qui exécute en concurrence toutes les coroutines/tâches passées, attend qu'elles soient toutes terminées, puis collecte tous les résultats.
  • asyncio.run(main_async()): l'approche recommandée à partir de Python 3.7+, responsable de la création de la boucle d'événements, de l'exécution de la coroutine et de la fermeture de la boucle.

Fonctions clés et descriptions des paramètres

Le tableau suivant répertorieasyncioplusieurs des fonctions de haut niveau les plus couramment utilisées :

Fonction Objectif principal Descriptions des paramètres courants
asyncio.run(coro, *, debug=False) Exécute une coroutine de niveau supérieur et gère le cycle de vie de la boucle d'événements. C'est le point d'entrée principal du programme. coro: l'objet coroutine à exécuter.
debug: défini àTruepour activer le mode débogage de la boucle d'événements.
asyncio.create_task(coro, *, name=None) Enveloppe une coroutine dans un objetTasket la met en file d'attente dans la boucle d'événements pour planification. C'est le principal moyen d'obtenir de la concurrence. coro: l'objet coroutine à envelopper.
name: (Python 3.8+) spécifiez un nom pour la tâche afin de faciliter le débogage.
asyncio.gather(*aws, return_exceptions=False) Exécute en parallèleplusieurs tâches asynchrones (awspeut accepter des coroutines, des tâches, etc.), attend que tout soit terminé et renvoie une liste de résultats. *aws: arguments variables, passez plusieurs objets asynchrones.
return_exceptions: la valeur par défaut estFalse, toute exception levée par une tâche sera immédiatement propagée àgatherl'appelant. Défini surTruelorsque cette option est définie, les exceptions seront renvoyées dans les résultats.
asyncio.sleep(delay, result=None) De manière asynchrone,suspend l'exécution pendant le nombre de secondes spécifié. C'est la principale différence avectime.sleep(bloquant). delay: Nombre de secondes de sommeil.
result: La valeur renvoyée après la fin du sommeil.
asyncio.wait(aws, *, timeout=None, return_when=ALL_COMPLETED) Exécute des tâches simultanément et attend que la condition spécifiée soit remplie. Renvoie deux ensembles(done, pending), qui sont respectivement les tâches terminées et non terminées. aws: Collection d’objets asynchrones.
timeout: Durée du délai d’attente (secondes).
return_when: Détermine quand renvoyer ; options :FIRST_COMPLETED(première terminée),FIRST_EXCEPTION(première exception),ALL_COMPLETED(toutes terminées, par défaut).
asyncio.to_thread(func, /, *args, **kwargs) (Python 3.9+) Exécute une fonction synchrone ordinaire, potentiellement bloquante, dans un thread séparé et renvoie unawaitcoroutine awaitable. Utilisée pour gérer les tâches intensives en CPU ou les E/S bloquantes. func: La fonction synchrone à exécuter dans le thread.
*args, **kwargs: Arguments passés à la fonction.

Compréhension visuelle : Flux d'ordonnancement des tâches asynchrones

Explication du diagramme: Ce diagramme montre comment la boucle d'événements fonctionne comme un répartiteur. Elle maintient une file de tâches. Lorsqu'une tâche s'exécute jusqu'àawait(par exemple, en attente d'une réponse réseau), elle est suspendue. La boucle d'événements trouve immédiatement la prochaine tâche exécutable (prête) dans la file et l'exécute. Lorsque l'opération d'E/S de la tâche suspendue se termine, la boucle d'événements reçoit une notification, remet le statut de la tâche à prêt et continue de l'exécuter à un moment futur. De cette façon, pendant les attentes d'E/S, le CPU est pleinement utilisé pour exécuter d'autres tâches, réalisant ainsi la concurrence au sein d'un seul thread.


Utilisation de base d'asyncio

1. Exécution d'une coroutine

Pour exécuter une coroutine, vous pouvez utiliser laasyncio.run()fonction. Elle crée une boucle d'événements et exécute la coroutine spécifiée.

Exemple

import asyncio

async def main():
    print("Start")
    await asyncio.sleep(1)
    print("End")

asyncio.run(main())

2. Exécution de plusieurs tâches en parallèle

Vous pouvez utiliser laasyncio.gather()fonction pour exécuter plusieurs coroutines en parallèle et attendre qu'elles se terminent toutes.

Exemple

import asyncio

async def task1():
    print("Task 1 started")
    await asyncio.sleep(1)
    print("Task 1 finished")

async def task2():
    print("Task 2 started")
    await asyncio.sleep(2)
    print("Task 2 finished")

async def main():
    await asyncio.gather(task1(), task2())

asyncio.run(main())

3. Contrôle des délais d'attente

Vous pouvez utiliser laasyncio.wait_for()fonction pour définir un délai d'attente pour une coroutine. Si la coroutine ne se termine pas dans le délai spécifié, uneasyncio.TimeoutErrorexception sera levée.

Exemple

import asyncio

async def long_task():
    await asyncio.sleep(10)
    print("Tâche terminée")

async def main():
    try:
        await asyncio.wait_for(long_task(), timeout=5)
    except asyncio.TimeoutError:
        print("La tâche a expiré")

asyncio.run(main())

Scénarios d'application d'asyncio

asyncioParticulièrement adapté aux scénarios suivants :

  1. Requêtes réseau: telles que les requêtes HTTP, la communication WebSocket, etc.
  2. E/S de fichiers: comme la lecture et l'écriture asynchrones de fichiers.
  3. Opérations de base de données: telles que l'accès asynchrone aux bases de données.
  4. Traitement de données en temps réel: tel que le traitement des files d'attente de messages en temps réel.

Classes, méthodes et fonctions courantes

1. Fonctions de base

Méthode/Fonction Description Exemple
asyncio.run(coro) Exécuter la fonction principale asynchrone (Python 3.7+) asyncio.run(main())
asyncio.create_task(coro) Créer une tâche et l'ajouter à la boucle d'événements task = asyncio.create_task(fetch_data())
asyncio.gather(*coros) Exécuter plusieurs coroutines de manière concurrente await asyncio.gather(task1, task2)
asyncio.sleep(delay) Attente asynchrone (non bloquante) await asyncio.sleep(1)
asyncio.wait(coros) Contrôler la façon dont les tâches se terminent done, pending = await asyncio.wait([task1, task2])

2. Boucle d'événements

Méthode Description Exemple
loop.run_until_complete(future) Exécuter jusqu'à ce que la tâche se termine loop.run_until_complete(main())
loop.run_forever() Exécuter la boucle d'événements indéfiniment loop.run_forever()
loop.stop() Arrêter la boucle d'événements loop.stop()
loop.close() Fermer la boucle d'événements loop.close()
loop.call_soon(callback) Planifier une fonction de rappel à exécuter immédiatement loop.call_soon(print, "Hello")
loop.call_later(delay, callback) Exécuter un callback avec un délai loop.call_later(5, callback)

3. Coroutines et tâches

Méthode/Décorateur Description Exemple
@asyncio.coroutine Décorateur de coroutine (hérité, Python 3.4-3.7) @asyncio.coroutine
def old_coro():
async def Définir une coroutine (Python 3.5+) async def fetch():
task.cancel() Annuler une tâche task.cancel()
task.done() Vérifier si une tâche est terminée if task.done():
task.result() Obtenir le résultat de la tâche (nécessite que la tâche soit terminée) data = task.result()

4. Primitives de synchronisation (similaires àthreading)

Classe Description Exemple
asyncio.Lock() Verrou mutex asynchrone lock = asyncio.Lock()
async with lock:
asyncio.Event() Notification d'événement event = asyncio.Event()
await event.wait()
asyncio.Queue() File d'attente asynchrone queue = asyncio.Queue()
await queue.put(item)
asyncio.Semaphore() Semaphore sem = asyncio.Semaphore(5)
async with sem:

5. Réseautage et sous-processus

Méthode/Classe Description Exemple
asyncio.open_connection() Établir une connexion TCP reader, writer = await asyncio.open_connection('host', 80)
asyncio.start_server() Créer un serveur TCP server = await asyncio.start_server(handle, '0.0.0.0', 8888)
asyncio.create_subprocess_exec() Créer un sous-processus proc = await asyncio.create_subprocess_exec('ls')

6. Outils utilitaires

Méthode Description Exemple
asyncio.current_task() Obtenir la tâche actuelle task = asyncio.current_task()
asyncio.all_tasks() Obtenir toutes les tâches tasks = asyncio.all_tasks()
asyncio.shield(coro) Protéger une tâche contre l'annulation await asyncio.shield(critical_task)
asyncio.wait_for(coro, timeout) Attendre avec un délai d'attente try: await asyncio.wait_for(task, 5)

Exemple

1. Exemple de coroutine de base

Exemple

import asyncio

async def hello():
    print("Hello")
    await asyncio.sleep(1)
    print("World")

asyncio.run(hello())  # Python 3.7+

2. Exécution concurrente de tâches

Exemple

async def fetch(url):
    print(f"Récupération de {url}")
    await asyncio.sleep(2)
    return f"Données de {url}"

async def main():
    results = await asyncio.gather(
        fetch("url1.com"),
        fetch("url2.com")
    )
    print(results)

asyncio.run(main())

3. Utilisation d'une file d'attente asynchrone

Exemple

async def producer(queue):
    for i in range(5):
        await queue.put(i)
        await asyncio.sleep(0.1)

async def consumer(queue):
    while True:
        item = await queue.get()
        print(f"Consumed {item}")
        queue.task_done()

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

Notes

  1. Version de Python: Certaines fonctionnalités nécessitent Python 3.7+ (commeasyncio.run())。

  2. Opérations bloquantes: Évitez d'utiliser du code bloquant synchrone dans les coroutines (commetime.sleep())。

  3. Débogage: Définissez laPYTHONASYNCIODEBUG=1variable d'environnement pour activer le mode débogage.

  4. Annulation de tâches: Une tâche annulée lèveraCancelledError, ce qui doit être géré correctement.

Autres extensions