【问题标题】:How to graceful shut down coroutines with Ctrl+C?如何使用 Ctrl+C 优雅地关闭协程?
【发布时间】:2018-01-10 17:57:48
【问题描述】:

我正在编写一个蜘蛛来抓取网页。我知道 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() 中添加了一些代码来优雅地关闭协程。但它不起作用。其他人有类似的问题吗?

【问题讨论】:

    标签: python python-asyncio coroutine


    【解决方案1】:

    我打赌问题发生在这里:

    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
    

    实际取消任务。

    更新:

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

    它依赖于你的任务状态:

    1. 如果此时按下“Ctrl+C”,所有任务都已完成,非 他们将在等待时引发CancelledError,您的代码将正常完成。
    2. 如果在您按下“Ctrl+C”的那一刻,一些任务处于待处理状态,但即将完成它们的执行,您的代码将在任务取消时卡住一点,并在任务完成后不久完成。
    3. 如果此时您按“Ctrl+C”某些任务处于待处理状态并且 远未完成,您的代码将卡住试图取消这些任务(其中 做不到)。另一个 'Ctrl+C' 将中断进程 取消,但任务不会被取消或完成,你会得到 警告“任务已被破坏,但它正在等待处理!”。

    【讨论】:

    • 我想你是对的。 'except: pass' 就是这样!我在'except:'中的'pass'之后添加'raise',它可以通过'Ctrl + C'很好地退出。因此,如果我想记录错误,我应该重新引发异常,以便 main() 可以捕获这些异常,包括 asyncio.CancelledError。但是我仍然很难理解为什么原始代码可以通过'Ctrl + C'以不确定的概率很好地退出?如果 fetch() 中的 'try-except' 结构可以捕获所有异常,则 main() 不会捕获任何异常,因此每次都会发生错误。
    • @xssl,我更新了答案以显示在不同情况下会发生什么。
    【解决方案2】:

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

    在终端中按 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 对它们执行硬杀。但是,这并没有给他们任何反应的选择,并且可能会导致问题。

    【讨论】:

      猜你喜欢
      • 2016-09-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-07-11
      • 2014-12-01
      • 2014-11-03
      • 1970-01-01
      • 2010-11-13
      相关资源
      最近更新 更多