web-dev-qa-db-fra.com

python asyncio, comment créer et annuler des tâches à partir d'un autre thread

J'ai une python application multi-thread. Je veux exécuter une boucle asyncio dans un thread et y poster des calbacks et des coroutines à partir d'un autre thread. Cela devrait être facile mais je ne peux pas me concentrer sur le asyncio des trucs.

Je suis venu à la solution suivante qui fait la moitié de ce que je veux, n'hésitez pas à commenter quoi que ce soit:

import asyncio
from threading import Thread

class B(Thread):
    def __init__(self):
        Thread.__init__(self)
        self.loop = None

    def run(self):
        self.loop = asyncio.new_event_loop()
        asyncio.set_event_loop(self.loop) #why do I need that??
        self.loop.run_forever()

    def stop(self):
        self.loop.call_soon_threadsafe(self.loop.stop)

    def add_task(self, coro):
        """this method should return a task object, that I
          can cancel, not a handle"""
        f = functools.partial(self.loop.create_task, coro)
        return self.loop.call_soon_threadsafe(f)

    def cancel_task(self, xx):
        #no idea

@asyncio.coroutine
def test():
    while True:
        print("running")
        yield from asyncio.sleep(1)

b.start()
time.sleep(1) #need to wait for loop to start
t = b.add_task(test())
time.sleep(10)
#here the program runs fine but how can I cancel the task?

b.stop()

Donc, démarrer et arrêter la boucle fonctionne bien. J'ai pensé à créer une tâche en utilisant create_task, mais cette méthode n'est pas threadsafe donc je l'ai enveloppée dans call_soon_threadsafe. Mais je voudrais pouvoir obtenir l'objet de tâche afin de pouvoir annuler la tâche. Je pourrais faire des choses compliquées en utilisant Future et Condition, mais il doit y avoir un moyen plus simple, n'est-ce pas?

25
Olivier RD

Je pense que vous devrez peut-être faire votre add_task méthode sachant si elle est appelée depuis un thread autre que celui de la boucle d'événements. De cette façon, s'il est appelé à partir du même thread, vous pouvez simplement appeler asyncio.async directement, sinon, il peut faire un travail supplémentaire pour passer la tâche du thread de la boucle au thread appelant. Voici un exemple:

import time
import asyncio
import functools
from threading import Thread, current_thread, Event
from concurrent.futures import Future

class B(Thread):
    def __init__(self, start_event):
        Thread.__init__(self)
        self.loop = None
        self.tid = None
        self.event = start_event

    def run(self):
        self.loop = asyncio.new_event_loop()
        asyncio.set_event_loop(self.loop)
        self.tid = current_thread()
        self.loop.call_soon(self.event.set)
        self.loop.run_forever()

    def stop(self):
        self.loop.call_soon_threadsafe(self.loop.stop)

    def add_task(self, coro):
        """this method should return a task object, that I
          can cancel, not a handle"""
        def _async_add(func, fut):
            try:
                ret = func()
                fut.set_result(ret)
            except Exception as e:
                fut.set_exception(e)

        f = functools.partial(asyncio.async, coro, loop=self.loop)
        if current_thread() == self.tid:
            return f() # We can call directly if we're not going between threads.
        else:
            # We're in a non-event loop thread so we use a Future
            # to get the task from the event loop thread once
            # it's ready.
            fut = Future()
            self.loop.call_soon_threadsafe(_async_add, f, fut)
            return fut.result()

    def cancel_task(self, task):
        self.loop.call_soon_threadsafe(task.cancel)


@asyncio.coroutine
def test():
    while True:
        print("running")
        yield from asyncio.sleep(1)

event = Event()
b = B(event)
b.start()
event.wait() # Let the loop's thread signal us, rather than sleeping
t = b.add_task(test()) # This is a real task
time.sleep(10)
b.stop()

Tout d'abord, nous enregistrons l'ID de thread de la boucle d'événements dans la méthode run, afin que nous puissions déterminer si les appels à add_task proviennent d'autres threads plus tard. Si add_task est appelé à partir d'un fil de boucle non événementiel, nous utilisons call_soon_threadsafe pour appeler une fonction qui planifiera à la fois la coroutine, puis utilisera un concurrent.futures.Future pour renvoyer la tâche au thread appelant, qui attend le résultat du Future.

Remarque sur l'annulation d'une tâche: lorsque vous appelez cancel sur un Task, un CancelledError sera levé dans la coroutine lors de la prochaine exécution de la boucle d'événement. Cela signifie que la coroutine que la tâche encapsule sera abandonnée en raison de l'exception la prochaine fois qu'elle atteindra un seuil de rendement - à moins que la coroutine n'attrape le CancelledError et ne s'interrompe elle-même. Notez également que cela ne fonctionne que si la fonction enveloppée est en fait une coroutine interruptible; une asyncio.Future renvoyé par BaseEventLoop.run_in_executor, par exemple, ne peut pas vraiment être annulé, car il est en fait enroulé autour d'un concurrent.futures.Future, et ceux-ci ne peuvent pas être annulés une fois que leur fonction sous-jacente a réellement commencé à s'exécuter. Dans ces cas, le asyncio.Future dira son annulé, mais la fonction en cours d'exécution dans l'exécuteur continuera de fonctionner.

Modifier: Mise à jour du premier exemple pour utiliser concurrent.futures.Future, à la place d'un queue.Queue, selon la suggestion d'Andrew Svetlov.

Remarque: asyncio.async est déconseillé car la version 3.4.4 utilise asyncio.ensure_future à la place.

17
dano

Tu fais tout bien. Pour la tâche d'arrêt de la méthode make

class B(Thread):
    # ...
    def cancel(self, task):
        self.loop.call_soon_threadsafe(task.cancel)

BTW vous avez pour configurer une boucle d'événements pour le thread créé explicitement par

self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)

car asyncio crée une boucle d'événement implicite uniquement pour le thread principal.

6
Andrew Svetlov

juste pour référence ici c'est le code que j'ai finalement implémenté en fonction de l'aide que j'ai eu sur ce site, c'est plus simple car je n'ai pas eu besoin de toutes les fonctionnalités. Merci encore!

import asyncio
from threading import Thread
from concurrent.futures import Future
import functools

class B(Thread):
    def __init__(self):
        Thread.__init__(self)
        self.loop = None

    def run(self):
        self.loop = asyncio.new_event_loop()
        asyncio.set_event_loop(self.loop)
        self.loop.run_forever()

    def stop(self):
        self.loop.call_soon_threadsafe(self.loop.stop)

    def _add_task(self, future, coro):
        task = self.loop.create_task(coro)
        future.set_result(task)

    def add_task(self, coro):
        future = Future()
        p = functools.partial(self._add_task, future, coro)
        self.loop.call_soon_threadsafe(p)
        return future.result() #block until result is available

    def cancel(self, task):
        self.loop.call_soon_threadsafe(task.cancel)
5
Olivier RD

Depuis la version 3.4.4 asyncio fournit une fonction appelée run_coroutine_threadsafe pour soumettre un objet coroutine d'un thread à une boucle d'événement. Il renvoie un concurrent.futures.Future pour accéder au résultat ou annuler la tâche.

En utilisant votre exemple:

@asyncio.coroutine
def test(loop):
    try:
        while True:
            print("Running")
            yield from asyncio.sleep(1, loop=loop)
    except asyncio.CancelledError:
        print("Cancelled")
        loop.stop()
        raise

loop = asyncio.new_event_loop()
thread = threading.Thread(target=loop.run_forever)
future = asyncio.run_coroutine_threadsafe(test(loop), loop)

thread.start()
time.sleep(5)
future.cancel()
thread.join()
3
Vincent