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