簡體   English   中英

如何中斷 Tornado 協程

[英]How to interrupt Tornado coroutine

假設我有兩個像這樣工作的函數:

@tornado.gen.coroutine
def f():
    for i in range(4):
        print("f", i)
        yield tornado.gen.sleep(0.5)

@tornado.gen.coroutine
def g():
    yield tornado.gen.sleep(1)
    print("Let's raise RuntimeError")
    raise RuntimeError

通常,函數f可能包含無限循環並且永不返回(例如,它可以處理某個隊列)。

我想要做的是能夠在它產生的任何時候中斷它。

最明顯的方法不起作用。 異常僅在函數f退出后引發(如果它是無窮無盡的,它顯然永遠不會發生)。

@tornado.gen.coroutine
def main():
    try:
        yield [f(), g()]
    except Exception as e:
        print("Caught", repr(e))

    while True:
        yield tornado.gen.sleep(10)

if __name__ == "__main__":
    tornado.ioloop.IOLoop.instance().run_sync(main)

輸出:

f 0
f 1
Let's raise RuntimeError
f 2
f 3
Traceback (most recent call last):
  File "/tmp/test/lib/python3.4/site-packages/tornado/gen.py", line 812, in run
    yielded = self.gen.send(value)
StopIteration

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  <...>
  File "test.py", line 16, in g
    raise RuntimeError
RuntimeError

也就是說,只有當兩個協程都返回(兩個期貨都解決)時才會引發異常。

這部分由tornado.gen.WaitIterator解決,但它有問題(除非我弄錯了)。 但這不是重點。

它仍然沒有解決中斷現有協程的問題。 即使啟動它的函數退出,協程也會繼續運行。

編輯:Tornado 似乎並不真正支持協程取消,這與 Python 的 asyncio 不同,在 Python 的 asyncio 中,您可以在每個屈服點輕松拋出CancelledError

如果您根據說明使用 WaitIterator ,並使用toro.Event在協程之間發出信號,它會按預期工作:

from datetime import timedelta
import tornado.gen
import tornado.ioloop

import toro

stop = toro.Event()


@tornado.gen.coroutine
def f():
    for i in range(4):
        print("f", i)

        # wait raises Timeout if not set before the deadline.
        try:
            yield stop.wait(timedelta(seconds=0.5))
            print("f done")
            return
        except toro.Timeout:
            print("f continuing")


@tornado.gen.coroutine
def g():
    yield tornado.gen.sleep(1)
    print("Let's raise RuntimeError")
    raise RuntimeError


@tornado.gen.coroutine
def main():
    wait_iterator = tornado.gen.WaitIterator(f(), g())
    while not wait_iterator.done():
        try:
            result = yield wait_iterator.next()
        except Exception as e:
            print("Error {} from {}".format(e, wait_iterator.current_future))
            stop.set()
        else:
            print("Result {} received from {} at {}".format(
                result, wait_iterator.current_future,
                wait_iterator.current_index))


if __name__ == "__main__":
    tornado.ioloop.IOLoop.instance().run_sync(main)

現在, pip install toro來獲取 Event 類。 Tornado 4.2 將包含事件, 請參閱更改日志

從版本 5 開始, Tornado 在asyncio事件循環上運行

在 Python 3 上, IOLoop始終是asyncio事件循環的包裝器,並且使用asyncio.Futureasyncio.Task代替它們的 Tornado 對應項。

因此您可以使用asyncio任務取消,即asyncio.Task.cancel

您的隊列讀取 while-true 循環示例可能如下所示。

import logging
from asyncio import CancelledError

from tornado import ioloop, gen


async def read_off_a_queue():
    while True:
        try:
            await gen.sleep(1)
        except CancelledError:
            logging.debug('Reader cancelled')
            break
        else:
            logging.debug('Pretend a task is consumed')

async def do_some_work():
    await gen.sleep(5)
    logging.debug('do_some_work is raising')
    raise RuntimeError                     

async def main():
    logging.debug('Starting queue reader in background')
    reader_task = gen.convert_yielded(read_off_a_queue())    
    try:
        await do_some_work()
    except RuntimeError:
        logging.debug('do_some_work failed, cancelling reader')
        reader_task.cancel()
        # give the task a chance to clean up, in case it
        # catches CancelledError and awaits something
        try:
            await reader_task            
        except CancelledError:
            pass


if __name__ == '__main__':
    logging.basicConfig(level='DEBUG')        
    ioloop.IOLoop.instance().run_sync(main)

如果你運行它,你應該看到:

DEBUG:asyncio:Using selector: EpollSelector
DEBUG:root:Starting queue reader in background
DEBUG:root:Pretend a task is consumed
DEBUG:root:Pretend a task is consumed
DEBUG:root:Pretend a task is consumed
DEBUG:root:Pretend a task is consumed
DEBUG:root:do_some_work is raising
DEBUG:root:do_some_work failed, cancelling reader
DEBUG:root:Reader cancelled

警告:這不是一個有效的解決方案。 看評論。 盡管如此,如果您是新手(就像我自己),這個例子可以顯示邏輯流程。 謝謝@nathaniel-j-smith 和@wgh

使用更原始的東西有什么區別,比如全局變量?

import asyncio


event = asyncio.Event()
aflag = False


async def short():
    while not aflag:
        print('short repeat')
        await asyncio.sleep(1)
    print('short end')


async def long():
    global aflag

    print('LONG START')
    await asyncio.sleep(3)

    aflag = True
    print('LONG END')


async def main():
    await asyncio.gather(long(), short())

if __name__ == '__main__':
    asyncio.run(main())

它用於asyncio ,但我想這個想法保持不變。 這是一個半問題(為什么Event會更好?)。 然而,解決方案產生了作者需要的確切結果:

LONG START
short repeat
short repeat
short repeat
LONG END
short end

更新:這張幻燈片可能對理解問題的核心很有幫助。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM