web-dev-qa-db-ja.com

asyncioコルーチンの正常なシャットダウン

現在、アプリケーションのCTRL-Cのシャットダウン中にasyncioコルーチンを閉じるのに問題があります。次のコードは、私が今持っているものの簡略版です。

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
import asyncio
import time
import functools
import signal


class DummyProtocol(asyncio.Protocol):

    def __init__(self, *args, **kwargs):
        self._shutdown = asyncio.Event()
        self._response = asyncio.Queue(maxsize=1)
        super().__init__(*args, **kwargs)

    def connection_made(self, transport):
        self.transport = transport

    def close(self):
        print("Closing protocol")
        self._shutdown.set()

    def data_received(self, data):

        #data = b'OK MPD '

        # Start listening for commands after a successful handshake
        if data.startswith(b'OK MPD '):
            print("Ready for sending commands")
            self._proxy_task = asyncio.ensure_future(self._send_commands())
            return

        # saving response for later consumption in self._send_commands
        self._response.put_nowait(data)

    async def _send_commands(self):

        while not self._shutdown.is_set():

            print("Waiting for commands coming in ...")

            command = None

            # listen for commands coming in from the global command queue. Only blocking 1sec.
            try:
                command = await asyncio.wait_for(cmd_queue.get(), timeout=1)
            except asyncio.TimeoutError:
                continue

            # sending the command over the pipe
            self.transport.write(command)

            # waiting for the response. Blocking until response is complete.
            res = await self._response.get()
            # put it into the global response queue
            res_queue.put_nowait(res)


async def connect(loop):
    c = lambda: DummyProtocol()
    t = asyncio.Task(loop.create_connection(c, '192.168.1.143', '6600'))
    try:
        # Wait for 3 seconds, then raise TimeoutError
        trans, proto = await asyncio.wait_for(t, timeout=3)
        print("Connected to <192.168.1.143:6600>.")
        return proto
    except (asyncio.TimeoutError, OSError) as e:
        print("Could not connect to <192.168.1.143:6600>. Trying again ...")
        if isinstance(e, OSError):
            log.exception(e)


def shutdown(proto, loop):
    # http://stackoverflow.com/a/30766124/1230358
    print("Shutdown of DummyProtocol initialized ...")
    proto.close()
    # give the coros time to finish
    time.sleep(2)

    # cancel all other tasks
    # for task in asyncio.Task.all_tasks():
    #    task.cancel()

    # stopping the event loop
    if loop:
        print("Stopping event loop ...")
        loop.stop()

    print("Shutdown complete ...")    


if __name__ == "__main__":

    loop = asyncio.get_event_loop()

    cmd_queue = asyncio.Queue()
    res_queue = asyncio.Queue()

    dummy_proto = loop.run_until_complete(connect(loop))

    for signame in ('SIGINT','SIGTERM'):
        loop.add_signal_handler(getattr(signal, signame), functools.partial(shutdown, dummy_proto, loop))

    try:
        loop.run_forever()
    except KeyboardInterrupt:
        pass
    finally:
        loop.close()

cTRL-Cが押された場合、次の出力が表示されます。

Connected to <192.168.1.143:6600>.
Ready for sending commands
Waiting for commands coming in ...
Waiting for commands coming in ...
Waiting for commands coming in ...
Waiting for commands coming in ...
^CShutdown of DummyProtocol initialized ...
Closing protocol
Stopping event loop ...
Shutdown complete ...
Task was destroyed but it is pending!
task: <Task pending coro=<DummyProtocol._send_commands() running at ./dummy.py:45> wait_for=<Future pending cb=[Task._wakeup()]>>
Task was destroyed but it is pending!
task: <Task pending coro=<Queue.get() running at /usr/local/Cellar/python3/3.5.1/Frameworks/Python.framework/Versions/3.5/lib/python3.5/asyncio/queues.py:168> wait_for=<Future pending cb=[Task._wakeup()]> cb=[_release_waiter(<Future pendi...sk._wakeup()]>)() at /usr/local/Cellar/python3/3.5.1/Frameworks/Python.framework/Versions/3.5/lib/python3.5/asyncio/tasks.py:344]>
Exception ignored in: <generator object Queue.get at 0x10594b468>
Traceback (most recent call last):
  File "/usr/local/Cellar/python3/3.5.1/Frameworks/Python.framework/Versions/3.5/lib/python3.5/asyncio/queues.py", line 170, in get
  File "/usr/local/Cellar/python3/3.5.1/Frameworks/Python.framework/Versions/3.5/lib/python3.5/asyncio/futures.py", line 227, in cancel
  File "/usr/local/Cellar/python3/3.5.1/Frameworks/Python.framework/Versions/3.5/lib/python3.5/asyncio/futures.py", line 242, in _schedule_callbacks
  File "/usr/local/Cellar/python3/3.5.1/Frameworks/Python.framework/Versions/3.5/lib/python3.5/asyncio/base_events.py", line 447, in call_soon
  File "/usr/local/Cellar/python3/3.5.1/Frameworks/Python.framework/Versions/3.5/lib/python3.5/asyncio/base_events.py", line 456, in _call_soon
  File "/usr/local/Cellar/python3/3.5.1/Frameworks/Python.framework/Versions/3.5/lib/python3.5/asyncio/base_events.py", line 284, in _check_closed
RuntimeError: Event loop is closed

私はasyncioをあまり経験していないので、ここで重要な何かを見逃していると確信しています。本当に頭痛の種になるのは、Shutdown complete ...の後の出力の一部です。 Task was destroyed but it is pending!から始めて、何が起こっているのかわからないことを認めなければなりません。私は他の質問を見ましたが、それを機能させることができませんでした。それで、なぜこのコードはTask was destroyed but it is pending! aso.のようなものを出力するのですか?コルーチンをきれいに閉じるにはどうすればいいですか?

ご協力いただきありがとうございます!

17
hetsch

_Task was destroyed but it is pending!_はどういう意味ですか?

プログラムがまだ終了していないasyncioタスクの一部を終了した場合、この警告が表示されます。実行中の一部のタスクが一部のリソースを正しく解放しない可能性があるため、この警告が必要です。

それを解決する一般的な方法は2つあります。

  1. タスクが完了するまで待つことができます
  2. タスクをキャンセルして、完了するまで待つことができます

Asyncioおよび同期操作のブロック

あなたのコードを見てみましょう:

_def shutdown(proto, loop):
    print("Shutdown of DummyProtocol initialized ...")
    proto.close()

    time.sleep(2)
    # ...
_

time.sleep(2)-この行は、コルーチンに終了する時間を与えません。すべてのプログラムを2秒間フリーズします。この間は何も起こりません。

これは、time.sleep(2)を呼び出すのと同じプロセスでイベントループが実行されているために発生します。 asyncioプログラムでは、この方法で長時間実行される同期操作を呼び出さないでください。 この回答 を読んで、非同期コードの動作を確認してください。

どうすればタスクが完了するのを待つことができますか

shutdown関数を変更してみましょう。これは非同期関数ではありません。内部に何かをawaitすることはできません。非同期コードを実行するには、手動で実行する必要があります:現在実行中のループを停止します(既に実行されているため)、タスクの完了を待機する非同期関数を作成し、イベントループで実行されるこの関数を渡します。

_def shutdown(proto, loop):
    print("Shutdown of DummyProtocol initialized ...")

    # Set shutdown event: 
    proto.close()

    # Stop loop:
    loop.stop()

    # Find all running tasks:
    pending = asyncio.Task.all_tasks()

    # Run loop until tasks done:
    loop.run_until_complete(asyncio.gather(*pending))

    print("Shutdown complete ...")    
_

タスクをキャンセルして、終了するのを待つこともできます。詳細については、 この回答 を参照してください。

クリーンアップ操作を配置する場所

私はシグナルに精通していませんが、CTRL-Cをキャッチするために本当に必要ですか? KeyboardInterruptが発生するたびに、イベントループを実行する行でスローされます(コードではloop.run_forever()です)。ここでは間違っているかもしれませんが、この状況を処理する一般的な方法は、すべてのクリーンアップ操作をfinallyブロックに配置することです。

たとえば、 can seeaiohttpの実行方法:

_try:
    loop.run_forever()
except KeyboardInterrupt:  # pragma: no branch
    pass
finally:
    srv.close()
    loop.run_until_complete(srv.wait_closed())
    loop.run_until_complete(app.shutdown())
    loop.run_until_complete(handler.finish_connections(shutdown_timeout))
    loop.run_until_complete(app.cleanup())
loop.close()
_
38