web-dev-qa-db-fra.com

Python - comment exécuter plusieurs coroutines simultanément en utilisant asyncio?

J'utilise la bibliothèque websockets pour créer un serveur websocket dans Python 3.4. Voici un serveur d'écho simple:

import asyncio
import websockets

@asyncio.coroutine
def connection_handler(websocket, path):
    while True:
        msg = yield from websocket.recv()
        if msg is None:  # connection lost
            break
        yield from websocket.send(msg)

start_server = websockets.serve(connection_handler, 'localhost', 8000)
asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()

Disons que nous - en plus - voulions envoyer un message au client chaque fois qu'un événement se produit. Par souci de simplicité, envoyons un message périodiquement toutes les 60 secondes. Comment ferions-nous cela? Je veux dire, parce que connection_handler attend constamment les messages entrants, le serveur ne peut agir qu'après avoir reçu un message du client, non? Qu'est-ce que j'oublie ici?

Peut-être que ce scénario nécessite un cadre basé sur des événements/rappels plutôt qu'un cadre basé sur des coroutines? Tornade ?

23
weatherfrog

TL; DR Utilisez asyncio.ensure_future() pour exécuter plusieurs coroutines simultanément.


Peut-être que ce scénario nécessite un cadre basé sur des événements/rappels plutôt qu'un cadre basé sur des coroutines? Tornade?

Non, vous n'avez besoin d'aucun autre framework pour cela. L'idée générale de l'application asynchrone vs synchrone est qu'elle ne se bloque pas, en attendant le résultat. Peu importe comment il est implémenté, en utilisant des coroutines ou des rappels.

Je veux dire, parce que connection_handler attend constamment les messages entrants, le serveur ne peut agir qu'après avoir reçu un message du client, non? Qu'est-ce que j'oublie ici?

Dans une application synchrone, vous écrirez quelque chose comme msg = websocket.recv(), qui bloquerait toute l'application jusqu'à ce que vous receviez un message (comme vous l'avez décrit). Mais dans l'application asynchrone, c'est complètement différent.

Quand vous faites msg = yield from websocket.recv() vous dites quelque chose comme: suspendre l'exécution de connection_handler() jusqu'à ce que websocket.recv() produise quelque chose. L'utilisation de yield from À l'intérieur de la coroutine renvoie le contrôle à la boucle d'événements, de sorte qu'un autre code peut être exécuté pendant que nous attendons le résultat de websocket.recv(). Veuillez vous référer à documentation pour mieux comprendre le fonctionnement des coroutines.

Disons que nous - en plus - voulions envoyer un message au client chaque fois qu'un événement se produit. Par souci de simplicité, envoyons un message périodiquement toutes les 60 secondes. Comment ferions-nous cela?

Vous pouvez utiliser asyncio.async() pour exécuter autant de coroutines que vous le souhaitez, avant d'exécuter l'appel de blocage pour démarrage de la boucle d'événements .

import asyncio

import websockets

# here we'll store all active connections to use for sending periodic messages
connections = []


@asyncio.coroutine
def connection_handler(connection, path):
    connections.append(connection)  # add connection to pool
    while True:
        msg = yield from connection.recv()
        if msg is None:  # connection lost
            connections.remove(connection)  # remove connection from pool, when client disconnects
            break
        else:
            print('< {}'.format(msg))
        yield from connection.send(msg)
        print('> {}'.format(msg))


@asyncio.coroutine
def send_periodically():
    while True:
        yield from asyncio.sleep(5)  # switch to other code and continue execution in 5 seconds
        for connection in connections:
            print('> Periodic event happened.')
            yield from connection.send('Periodic event happened.')  # send message to each connected client


start_server = websockets.serve(connection_handler, 'localhost', 8000)
asyncio.get_event_loop().run_until_complete(start_server)
asyncio.async(send_periodically())  # before blocking call we schedule our coroutine for sending periodic messages
asyncio.get_event_loop().run_forever()

Voici un exemple d'implémentation client. Il vous demande d'entrer un nom, le reçoit du serveur d'écho, attend deux autres messages du serveur (qui sont nos messages périodiques) et ferme la connexion.

import asyncio

import websockets


@asyncio.coroutine
def hello():
    connection = yield from websockets.connect('ws://localhost:8000/')
    name = input("What's your name? ")
    yield from connection.send(name)
    print("> {}".format(name))
    for _ in range(3):
        msg = yield from connection.recv()
        print("< {}".format(msg))

    yield from connection.close()


asyncio.get_event_loop().run_until_complete(hello())

Les points importants:

  1. Dans Python 3.4.4 asyncio.async() a été renommé asyncio.ensure_future() .
  2. Il existe des méthodes spéciales pour planifier appels retardés , mais elles ne fonctionnent pas avec les coroutines.
28
Yaroslav Admin

Même problème, je peux difficilement trouver de solution avant d'avoir vu l'exemple parfait ici: http://websockets.readthedocs.io/en/stable/intro.html#both

 done, pending = await asyncio.wait(
        [listener_task, producer_task],
        return_when=asyncio.FIRST_COMPLETED)  # Important

Ainsi, je peux gérer des tâches multi-coroutines telles que le battement de coeur et le redis subscribe.

7
gzerone

Je suis surpris gather n'est pas mentionné.

De la documentation Python :

import asyncio

async def factorial(name, number):
    f = 1
    for i in range(2, number + 1):
        print(f"Task {name}: Compute factorial({i})...")
        await asyncio.sleep(1)
        f *= i
    print(f"Task {name}: factorial({number}) = {f}")

async def main():
    # Schedule three calls *concurrently*:
    await asyncio.gather(
        factorial("A", 2),
        factorial("B", 3),
        factorial("C", 4),
    )

asyncio.run(main())

# Expected output:
#
#     Task A: Compute factorial(2)...
#     Task B: Compute factorial(2)...
#     Task C: Compute factorial(2)...
#     Task A: factorial(2) = 2
#     Task B: Compute factorial(3)...
#     Task C: Compute factorial(3)...
#     Task B: factorial(3) = 6
#     Task C: Compute factorial(4)...
#     Task C: factorial(4) = 24
0
Cyril N.