web-dev-qa-db-ja.com

python asyncio、別のスレッドからタスクを作成およびキャンセルする方法

pythonマルチスレッドアプリケーションです。スレッドでasyncioループを実行し、別のスレッドからコールバックとコルーチンをポストしたいのですが、簡単なはずですが、頭がよくありません。 asyncio もの。

私は私が望むことの半分を行う次の解決策にたどり着きました、何でも気軽にコメントしてください:

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を使用してタスクを作成することを考えましたが、そのメソッドはスレッドセーフではないため、call_soon_threadsafeでラップしました。しかし、タスクをキャンセルできるようにするために、タスクオブジェクトを取得できるようにしたいと思います。 FutureとConditionを使用して複雑なことを行うこともできますが、もっと簡単な方法があるはずですよね。

25
Olivier RD

イベントループ以外のスレッドから呼び出されているかどうかを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()

最初に、イベントループのスレッドIDをrunメソッドに保存します。これにより、add_taskの呼び出しが後で他のスレッドから行われているかどうかを確認できます。非イベントループスレッドからadd_taskが呼び出された場合、call_soon_threadsafeを使用してコルーチンをスケジュールする関数を呼び出し、次にconcurrent.futures.Futureを使用してタスクをFutureの結果を待つ呼び出しスレッド.

タスクのキャンセルに関する注意:cancelに対してTaskを呼び出すと、次にイベントループが実行されるときにコルーチンでCancelledErrorが発生します。これは、タスクがラップしているコルーチンが、次に降伏点に達したときに例外のために中止されることを意味します-コルーチンがCancelledErrorをキャッチして自身が中止されない限り。また、これは、ラップされる関数が実際に割り込み可能なコルーチンである場合にのみ機能することにも注意してください。たとえば、asyncio.Futureによって返されたBaseEventLoop.run_in_executorは、実際にはconcurrent.futures.Futureでラップされているため、実際にキャンセルすることはできません。また、基礎となる関数が実際に実行を開始するとキャンセルすることはできません。 。これらの場合、asyncio.Futureはキャンセルされたと言いますが、エグゼキューターで実際に実行されている関数は引き続き実行されます。

編集:Andrew Svetlovの提案に従い、concurrent.futures.Futureではなくqueue.Queueを使用するように最初の例を更新しました。

注: asyncio.async はバージョン3.4.4で廃止されました。代わりに asyncio.ensure_future を使用してください。

17
dano

あなたはすべてを正しく行います。タスク停止用makeメソッド

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

ところであなたhave作成されたスレッドのイベントループを明示的に設定する

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

asyncioは、メインスレッドに対してのみ暗黙的なイベントループを作成するためです。

6
Andrew Svetlov

参考までに、このサイトで得たヘルプに基づいて最終的に実装したコードは、すべての機能を必要としなかったので、より単純です。再度、感謝します!

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

バージョン3.4.4以降、asyncioには、スレッドからイベントループにコルーチンオブジェクトを送信するための run_coroutine_threadsafe という関数が用意されています。結果にアクセスするか、タスクをキャンセルするために concurrent.futures.Future を返します。

あなたの例を使用して:

@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