Python 如何使用Ctrl+;C

Python 如何使用Ctrl+;C,python,python-asyncio,coroutine,Python,Python Asyncio,Coroutine,我正在写一个蜘蛛来抓取网页。我知道asyncio可能是我最好的选择。因此,我使用协程异步处理工作。现在我开始琢磨如何通过键盘中断退出程序。所有工作完成后,程序可能会很快关闭。源代码可以在Python3.5中运行,并附在下面 import asyncio import aiohttp from contextlib import suppress class Spider(object): def __init__(self): self.max_tasks = 2

我正在写一个蜘蛛来抓取网页。我知道asyncio可能是我最好的选择。因此,我使用协程异步处理工作。现在我开始琢磨如何通过键盘中断退出程序。所有工作完成后,程序可能会很快关闭。源代码可以在Python3.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()中添加了一些代码以优雅地关闭协同程序。但它不起作用。其他人也有类似的问题吗?

我打赌问题发生在这里:

except:
    pass
你知道这样的事。你的情况是另外一个例子,说明了如果不是这样的话会发生什么

当您取消任务并等待其取消时,
asyncio.canceledError
在任务内部引发,并在任务内部的任何位置抑制。等待任务取消的行应引发此异常,否则任务将继续执行

这就是你为什么这么做的原因

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

Upd:

但是我仍然很难理解为什么原始代码可以在 “Ctrl+C”的概率不确定

它取决于任务的状态:

  • 如果此时按下“Ctrl+C”键,所有任务都已完成,则不包括 他们将在等待时引发
    cancelled错误
    ,您的代码将正常完成
  • 如果此时按“Ctrl+C”键,某些任务处于挂起状态,但接近完成它们的执行,则代码将在任务取消时卡住一点,并在任务完成后不久完成
  • 如果此时按下“Ctrl+C”,则某些任务处于挂起状态,并且 远未完成,您的代码将在尝试取消这些任务时卡住(这 做不到)。另一个“Ctrl+C”将中断 取消,但任务不会被取消或完成,然后您将获得 警告“任务已销毁,但正在挂起!”
    我假设您正在使用任何风格的Unix;如果不是这样,我的评论可能不适用于你的情况

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

    然后,线程管理将等待仍在运行的其他线程终止,然后Unix进程作为一个整体以退出代码终止。这可能需要相当长的时间。请参阅,以及为什么这在一般情况下是不可能的

    我认为,您要做的是立即终止进程,一步终止所有线程

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

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

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

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

    我想你是对的除了:通行证是这样!我在“pass”后面加上了“raise”,除了:“而且它可以通过“Ctrl+C”很好地退出。”。因此,如果我想记录错误,我应该重新调用异常,以便main()可以捕获这些异常,包括asyncio.CancelleError。但是我仍然很难理解为什么原始代码能够以不确定的概率通过“Ctrl+C”很好地退出?如果fetch()中的“try except”结构可以捕获所有异常,main()将不会捕获任何异常,因此每次都会发生错误。@xssl,我更新了答案以显示在不同情况下可能发生的情况。