简体   繁体   English

优雅关闭 asyncio 协程

[英]Graceful shutdown of asyncio coroutines

I'm currently having problems closing asyncio coroutines during the shutdown CTRL-C of an application.我目前在关闭应用程序的 CTRL-C 期间关闭 asyncio 协程时遇到问题。 The following code is a stripped down version of what I have right now:以下代码是我现在拥有的精简版本:

#!/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()

what gives me the following output if CTRL-C is pressed:如果按下 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

I'm not very experienced with asyncio, so I'm pretty sure that I'm missing something important here.我对 asyncio 不是很有经验,所以我很确定我在这里遗漏了一些重要的东西。 What really gives me headaches is the part of the output after Shutdown complete ... .真正让我头疼的是Shutdown complete ...后的输出部分。 Beginning with Task was destroyed but it is pending!Task was destroyed but it is pending! , I have to admit that I have no idea what's going on. ,我不得不承认我不知道发生了什么。 I had a look on other questions but couldn't get it to work.我查看了其他问题,但无法正常工作。 So, why is this code outputting stuff like Task was destroyed but it is pending! aso.那么,为什么这段代码输出的东西像Task was destroyed but it is pending! aso. Task was destroyed but it is pending! aso. and how can cleany close the coroutines? cleany 如何关闭协程?

Thank's for your help!谢谢你的帮助!

What does Task was destroyed but it is pending! Task was destroyed but it is pending!什么意思? mean?意思是?

If at the moment your program finished some of asyncio tasks still not finished, you'll get this warning.如果此时您的程序完成了一些 asyncio 任务仍未完成,您将收到此警告。 This warning is needed because some task that are running may not correctly free some resources.需要此警告是因为某些正在运行的任务可能无法正确释放某些资源。

There're two common ways to solve it:有两种常见的方法来解决它:

  1. You can wait while tasks finished themselves您可以等待任务自行完成
  2. You can cancel tasks and wait while them finished您可以取消任务并等待它们完成

Asyncio and blocking synchronous operations异步和阻塞同步操作

Let's look at you code:让我们看看你的代码:

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

    time.sleep(2)
    # ...

time.sleep(2) - this line won't give coroutines time to finished. time.sleep(2) - 这一行不会给协程时间来完成。 It'll just freeze all your program for two second.它只会冻结你所有的程序两秒钟。 Nothing will happen during this time.在此期间什么都不会发生。

It happens because your event loop is running in same process where you call time.sleep(2) .发生这种情况是因为您的事件循环在您调用time.sleep(2)的同一进程中运行。 You should never call long running synchronous operations this way in your asyncio programs.你不应该在你的 asyncio 程序中以这种方式调用长时间运行的同步操作。 Please read this answer to see how async code works.请阅读此答案以了解异步代码的工作原理。

How can we wait tasks finished我们如何等待任务完成

Let's try to modify shutdown function.让我们尝试修改shutdown功能。 This is not async function, you can't await something inside it.这不是异步函数,你不能await它里面的东西。 To execute some async code we need to do it manually: stop currently running loop (since it's already running), create some async function to await tasks finished, pass this function to be executed in event loop.要执行一些异步代码,我们需要手动执行:停止当前正在运行的循环(因为它已经在运行),创建一些异步函数来等待任务完成,传递此函数以在事件循环中执行。

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

    # Set shutdown event: 
    proto.close()

    # Stop loop:
    loop.stop()

    # Find all running tasks:
    # For python version < 3.7 use asyncio.Task.all_tasks()
    # For python version >= 3.7 use asyncio.all_tasks()
    pending = asyncio.Task.all_tasks()

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

    print("Shutdown complete ...")    

You can also just cancel tasks and await them finished.您也可以取消任务并等待它们完成。 See this answer for details.有关详细信息,请参阅此答案

Where to place clean up operations在哪里进行清理操作

I'm not too familiar with signals, but do you really need it to catch CTRL-C?我对信号不太熟悉,但你真的需要它来捕捉 CTRL-C 吗? Whenever KeyboardInterrupt happens it will thrown at the line where you run the event loop (in you code it's loop.run_forever() ).每当KeyboardInterrupt发生时,它都会在你运行事件循环的那一行抛出(在你的代码中它是loop.run_forever() )。 I might be wrong here, but a common way to handle this situation is to place all cleanup operations in a finally block.我在这里可能错了,但处理这种情况的常用方法是将所有清理操作放在finally块中。

For example, you can see how aiohttp does it:例如,您可以看到aiohttp是如何做到的:

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

要完成已接受的答案,您可以使用aiorun很好地为您处理此问题: https ://github.com/cjrh/aiorun

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM