python asyncio, как создавать и отменять задачи из другого потока

У меня есть многопоточное приложение python. Я хочу запустить цикл asyncio в потоке и отправить ему calbacks и coroutines из другого потока. Должно быть легко, но я не могу получить мою голову вокруг ввода-вывода вещи.

Я придумал следующее решение, которое делает половину того, что я хочу, не стесняйтесь комментировать что-либо:

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()

таким образом, запуск и остановка цикла работает нормально. Я думал о создании задачи с помощью create_task, но этот метод не threadsafe, поэтому я завернул его в call_soon_threadsafe. Но я хотел бы иметь возможность получить объект задачи, чтобы иметь возможность отменить задачу. Я мог бы сделать сложную вещь, используя будущее и состояние, но должен быть более простой способ, не так ли?

4 ответов


я думаю, вам может понадобиться сделать ваш add_task метод знает, вызывается ли он из потока, отличного от цикла событий. Таким образом, если он вызывается из того же потока, вы можете просто вызвать asyncio.async напрямую, иначе он может выполнить дополнительную работу, чтобы передать задачу из потока цикла в вызывающий поток. Вот пример:

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()

во-первых, мы сохраняем идентификатор потока цикла событий в run метод, поэтому мы можем выяснить, вызывает ли add_task приходят из других потоков позже. Если add_task вызывается из потока цикла без событий, мы используем call_soon_threadsafe для вызова функции, которая будет как планировать сопрограмму, а затем использовать concurrent.futures.Future чтобы передать задачу обратно вызывающему потоку, который ожидает результата Future.

примечание об отмене задачи: вы, когда вы звоните cancel на Task, a CancelledError будет поднят в coroutine при следующем запуске цикла событий. Это означает, что coroutine, что задача обертывание будет прервано из - за исключения в следующий раз, когда он достигнет точки выхода-если корутина не поймает CancelledError и предотвращает себя от аборта. Также обратите внимание, что это работает только в том случае, если обернутая функция на самом деле является прерываемой корутиной;asyncio.Future возвращено BaseEventLoop.run_in_executor, например, на самом деле не может быть отменен, потому что он фактически обернут вокруг concurrent.futures.Future, и они не могут быть отменены, как только их базовая функция фактически начнет выполняться. В этих случаях asyncio.Future скажет, что его отменили, но функция, фактически работающая в исполнителе, будет продолжать работать.

Edit: обновлен первый пример использования concurrent.futures.Future, вместо queue.Queue, по предложению Андрея Светлова.

Примечание: asyncio.async устарел с версии 3.4.4 use asyncio.ensure_future.


вы все делаете правильно. Для остановки задачи make method

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

кстати,есть чтобы настроить цикл событий для созданного потока явно с помощью

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

, потому что asyncio создает неявный цикл событий только для основного потока.


просто для справки здесь это код, который я наконец реализовал на основе справки, которую я получил на этом сайте, это проще, так как мне не нужны все функции. еще раз спасибо!

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)

начиная с версии 3.4.4 asyncio предоставляет функцию с именем run_coroutine_threadsafe для отправки объекта coroutine из потока в цикл событий. Он возвращает одновременно.фьючерсный.Будущее для доступа к результату или отмены задачи.

используя Ваш пример:

@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()