如何使用Ctrl + C优雅地关闭协同程序?

时间:2017-08-03 08:34:49

标签: python python-asyncio coroutine

我正在写蜘蛛来抓取网页。我知道asyncio可能是我最好的选择。所以我使用协同程序异步处理工作。现在我抓住了关于如何通过键盘中断退出程序的问题。所有工作完成后,该程序可能会关闭。源代码可以在python 3.5中运行,并在下面附上。

import asyncio
import aiohttp
from contextlib import suppress

class Spider(object):
    def __init__(self):
        self.max_tasks = 2
        self.task_queue = asyncio.Queue(self.max_tasks)
        self.loop = asyncio.get_event_loop()
        self.counter = 1

    def close(self):
        for w in self.workers:
            w.cancel()

    async def fetch(self, url):
        try:
            async with aiohttp.ClientSession(loop = self.loop) as self.session:
                with aiohttp.Timeout(30, loop = self.session.loop):
                    async with self.session.get(url) as resp:
                        print('get response from url: %s' % url)
        except:
            pass
        finally:
            pass

    async def work(self):
        while True:
            url = await self.task_queue.get()
            await self.fetch(url)
            self.task_queue.task_done()

    def assign_work(self):
        print('[*]assigning work...')
        url = 'https://www.python.org/'
        if self.counter > 10:
            return 'done'
        for _ in range(self.max_tasks):
            self.counter += 1
            self.task_queue.put_nowait(url)

    async def crawl(self):
        self.workers = [self.loop.create_task(self.work()) for _ in range(self.max_tasks)]
        while True:
            if self.assign_work() == 'done':
                break
            await self.task_queue.join()
        self.close()

def main():
    loop = asyncio.get_event_loop()
    spider = Spider()
    try:
        loop.run_until_complete(spider.crawl())
    except KeyboardInterrupt:
        print ('Interrupt from keyboard')
        spider.close()
        pending  = asyncio.Task.all_tasks()
        for w in pending:
            w.cancel()
            with suppress(asyncio.CancelledError):
                loop.run_until_complete(w)
    finally:
        loop.stop()
        loop.run_forever()
        loop.close()

if __name__ == '__main__':
    main()

但如果我在运行时按“Ctrl + C”,可能会出现一些奇怪的错误。我的意思是有时程序可以通过'Ctrl + C'优雅地关闭。没有错误消息。但是,在某些情况下,按“Ctrl + C”后程序仍将运行,并且在完成所有工作之前不会停止。如果我在那一刻按下“Ctrl + C”,“任务已被销毁,但它正在等待!”会在那里。

我已经阅读了一些关于asyncio的主题,并在main()中添加了一些代码来优雅地关闭协同程序。但它不起作用。其他人是否有类似的问题?

2 个答案:

答案 0 :(得分:3)

我打赌问题发生在这里:

except:
    pass

should never do这样的事情。而你的情况就是另外一个例子。

当您取消任务并等待取消时,asyncio.CancelledError在任务内部被提升,而shouldn't be被禁止在任何内部。等待任务取消的行应该引发此异常,否则任务将继续执行。

这就是你做的原因

task.cancel()
with suppress(asyncio.CancelledError):
    loop.run_until_complete(task)  # this line should raise CancelledError, 
                                   # otherwise task will continue

实际取消任务。

<强> UPD:

  

但我仍然不明白为什么原始代码可以很好地退出   'Ctrl + C'的概率不确定?

它依赖于你的任务状态:

  1. 如果此时按“Ctrl + C”,则完成所有任务,而不是 他们将等待CancelledError,你的代码将正常完成。
  2. 如果你按“Ctrl + C”,那么一些任务正在等待,但是接近完成执行,你的代码会在任务取消时停留一些,并在任务完成后立即完成。
  3. 如果此时按“Ctrl + C”,某些任务正在等待处理 远远没有完成,你的代码将卡住尝试取消这些任务(其中 无法做到)。另一个'Ctrl + C'将中断进程 取消,但任务不会被取消或完成然后你会得到 警告'任务已被破坏,但它正在等待!'。

答案 1 :(得分:0)

我假设您正在使用任何Unix风格;如果不是这种情况,我的评论可能不适用于您的情况。

在终端中按 Ctrl - C 会发送与此tty相关联的所有进程信号SIGINT。 Python进程捕获此Unix信号并将其转换为抛出KeyboardInterrupt异常。在一个线程化的应用程序中(我不确定内部的async内容是否正在使用线程,但它听起来很像)通常只有一个线程(主线程)接收到此信号,因此以这种方式作出反应。如果没有特别针对这种情况做好准备,它将因例外而终止。

然后,线程管理将等待仍然运行的伙伴线程在Unix进程之前终止,因为整个程序以退出代码终止。这可能需要相当长的时间。请参阅this question about killing fellow threads以及为何一般无法做到这一点。

我想你要做的是立即杀死你的进程,一步杀死所有线程。

实现此目的的最简单方法是按 Ctrl - \ 。这将发送SIGQUIT而不是SIGINT,这通常也会影响其他线程并导致它们终止。

如果这还不够(因为无论出于何种原因你需要在 Ctrl - C 上做出正确反应),你可以给自己发一个信号:

import os, signal

os.kill(os.getpid(), signal.SIGQUIT)

这应该终止所有正在运行的线程,除非它们特别捕获SIGQUIT,在这种情况下你仍然可以使用SIGKILL对它们执行硬杀死。但是,这并没有给他们任何反应选择,并且可能会导致问题。