web-dev-qa-db-ja.com

中断されたイベントループの後にクリーンアップする正しい方法は何ですか?

コマンドラインツールの一部としていくつかのコルーチンを実行するイベントループがあります。ユーザーは通常の方法でツールを中断できます Ctrl + C、その時点で、中断されたイベントループの後に適切にクリーンアップする必要があります。

これが私が試したものです。

_import asyncio


@asyncio.coroutine
def shleepy_time(seconds):
    print("Shleeping for {s} seconds...".format(s=seconds))
    yield from asyncio.sleep(seconds)


if __== '__main__':
    loop = asyncio.get_event_loop()

    # Side note: Apparently, async() will be deprecated in 3.4.4.
    # See: https://docs.python.org/3.4/library/asyncio-task.html#asyncio.async
    tasks = [
        asyncio.async(shleepy_time(seconds=5)),
        asyncio.async(shleepy_time(seconds=10))
    ]

    try:
        loop.run_until_complete(asyncio.gather(*tasks))
    except KeyboardInterrupt as e:
        print("Caught keyboard interrupt. Canceling tasks...")

        # This doesn't seem to be the correct solution.
        for t in tasks:
            t.cancel()
    finally:
        loop.close()
_

これを実行して打つ Ctrl + C 収量:

_$ python3 asyncio-keyboardinterrupt-example.py 
Shleeping for 5 seconds...
Shleeping for 10 seconds...
^CCaught keyboard interrupt. Canceling tasks...
Task was destroyed but it is pending!
task: <Task pending coro=<shleepy_time() running at asyncio-keyboardinterrupt-example.py:7> wait_for=<Future cancelled> cb=[gather.<locals>._done_callback(1)() at /usr/local/Cellar/python3/3.4.3/Frameworks/Python.framework/Versions/3.4/lib/python3.4/asyncio/tasks.py:587]>
Task was destroyed but it is pending!
task: <Task pending coro=<shleepy_time() running at asyncio-keyboardinterrupt-example.py:7> wait_for=<Future cancelled> cb=[gather.<locals>._done_callback(0)() at /usr/local/Cellar/python3/3.4.3/Frameworks/Python.framework/Versions/3.4/lib/python3.4/asyncio/tasks.py:587]>
_

明らかに、私は正しくクリーンアップしませんでした。おそらく、タスクでcancel()を呼び出すことがそれを行う方法だと思いました。

中断されたイベントループの後にクリーンアップする正しい方法は何ですか?

37
Nick Chammas

Ctrl + Cキーを押すと、イベントループが停止するため、t.cancel()への呼び出しは実際には有効になりません。タスクをキャンセルするには、ループバックを再度開始する必要があります。

処理方法は次のとおりです。

_import asyncio

@asyncio.coroutine
def shleepy_time(seconds):
    print("Shleeping for {s} seconds...".format(s=seconds))
    yield from asyncio.sleep(seconds)


if __== '__main__':
    loop = asyncio.get_event_loop()

    # Side note: Apparently, async() will be deprecated in 3.4.4.
    # See: https://docs.python.org/3.4/library/asyncio-task.html#asyncio.async
    tasks = asyncio.gather(
        asyncio.async(shleepy_time(seconds=5)),
        asyncio.async(shleepy_time(seconds=10))
    )

    try:
        loop.run_until_complete(tasks)
    except KeyboardInterrupt as e:
        print("Caught keyboard interrupt. Canceling tasks...")
        tasks.cancel()
        loop.run_forever()
        tasks.exception()
    finally:
        loop.close()
_

KeyboardInterruptをキャッチしたら、tasks.cancel()を呼び出してからloopを再び起動します。 _run_forever_は、tasksがキャンセルされるとすぐに実際に終了します(_asyncio.gather_によって返されるFutureをキャンセルすると、その中のFuturesもすべてキャンセルされます。 )、中断された_loop.run_until_complete_呼び出しが、ループを停止するtasksに_done_callback_を追加したためです。したがって、tasksをキャンセルすると、そのコールバックが起動し、ループが停止します。その時点で、_tasks.exception_から例外を取得しないことに関する警告が表示されないようにするために、__GatheringFuture_を呼び出します。

39
dano

Python 3.6 +に更新されました:_loop.shutdown_asyncgens_への呼び出しを追加して、非同期ジェネレーターによるメモリリークを回避しましたさらに、最後の_asyncio.new_event_loop_呼び出しがループの他の使用の可能性を妨げないようにするために、_asyncio.get_event_loop_ではなく_loop.close_が使用されるようになりました。

他の回答のいくつかに触発された次のソリューションは、ほとんどすべての場合に機能するはずであり、クリーンアップする必要があるタスクを手動で追跡することに依存しません Ctrl+C

_loop = asyncio.new_event_loop()
try:
    # Here `amain(loop)` is the core coroutine that may spawn any
    # number of tasks
    sys.exit(loop.run_until_complete(amain(loop)))
except KeyboardInterrupt:
    # Optionally show a message if the shutdown may take a while
    print("Attempting graceful shutdown, press Ctrl+C again to exit…", flush=True)

    # Do not show `asyncio.CancelledError` exceptions during shutdown
    # (a lot of these may be generated, skip this if you prefer to see them)
    def shutdown_exception_handler(loop, context):
        if "exception" not in context \
        or not isinstance(context["exception"], asyncio.CancelledError):
            loop.default_exception_handler(context)
    loop.set_exception_handler(shutdown_exception_handler)

    # Handle shutdown gracefully by waiting for all tasks to be cancelled
    tasks = asyncio.gather(*asyncio.Task.all_tasks(loop=loop), loop=loop, return_exceptions=True)
    tasks.add_done_callback(lambda t: loop.stop())
    tasks.cancel()

    # Keep the event loop running until it is either destroyed or all
    # tasks have really terminated
    while not tasks.done() and not loop.is_closed():
        loop.run_forever()
finally:
    if hasattr(loop, "shutdown_asyncgens"):  # This check is only needed for Python 3.5 and below
        loop.run_until_complete(loop.shutdown_asyncgens())
    loop.close()
_

上記のコードは、現在のすべてのタスクを_asyncio.Task.all_tasks_を使用してイベントループから取得し、_asyncio.gather_を使用して単一の結合された将来に配置します。そのフューチャーのすべてのタスク(現在実行中のすべてのタスク)は、フューチャーの.cancel()メソッドを使用してキャンセルされます。 _return_exceptions=True_を使用すると、futureにエラーが発生する代わりに、受信した_asyncio.CancelledError_例外がすべて保存されます。

上記のコードは、デフォルトの例外ハンドラをオーバーライドして、生成された_asyncio.CancelledError_例外がログに記録されないようにします。

12
ntninja

Windowsを使用している場合を除き、SIGINT(およびサービスとして実行できるようにSIGTERMも)のイベントループベースのシグナルハンドラを設定します。これらのハンドラーでは、イベントループをすぐに終了するか、何らかのクリーンアップシーケンスを開始して後で終了することができます。

公式の例Python documentation: https://docs.python.org/3.4/library/asyncio-eventloop.html#set-signal-handlers-for-sigint-and -sigterm

3
Ambroz Bizjak