如何使用 Ctrl+C 优雅地关闭协程?
How to graceful shut down coroutines with Ctrl+C?
我正在写一个蜘蛛来抓取网页。我知道 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()
但是如果我在 运行ning 时按 'Ctrl+C',可能会出现一些奇怪的错误。我的意思是有时 'Ctrl+C' 可以正常关闭程序。没有错误信息。然而,在某些情况下,程序在按下 'Ctrl+C' 后仍会 运行ning 并且在所有工作完成之前不会停止。如果我在那一刻按'Ctrl+C','Task was destroyed but it is pending!'就会在那里。
我已经阅读了一些关于 asyncio 的主题,并在 main() 中添加了一些代码以优雅地关闭协程。但它不起作用。有没有人遇到类似的问题?
我打赌问题出在这里:
except:
pass
你should never do这种东西。你的情况是另一种可能发生的情况的例子。
当您取消任务并等待其取消时,asyncio.CancelledError
在任务内部引发, 在内部任何地方抑制。您等待任务取消的行应引发此异常,否则任务将继续执行。
这就是你这样做的原因
task.cancel()
with suppress(asyncio.CancelledError):
loop.run_until_complete(task) # this line should raise CancelledError,
# otherwise task will continue
实际取消任务。
更新:
But I still hardly understand why the original code could quit well by
'Ctrl+C' at a uncertain probability?
它依赖于你的任务状态:
- 如果此时你按下'Ctrl+C'所有任务都完成了,没有
他们将在等待时提高
CancelledError
,您的代码将正常完成。
- 如果在您按下 'Ctrl+C' 时某些任务处于待处理状态,但接近于完成它们的执行,您的代码将在任务取消时卡住一点,并在任务完成后不久完成。
- 如果此时您按下 'Ctrl+C' 某些任务正在等待处理并且
远未完成,您的代码将在尝试取消这些任务时卡住(这
无法完成)。另一个 'Ctrl+C' 将中断进程
取消,但任务不会被取消或完成然后你会得到
警告 'Task was destroyed but it is pending!'.
我假设您正在使用任何风格的 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
对它们执行硬杀。但是,这不会给他们任何反应的选择,并且可能会导致问题。
我正在写一个蜘蛛来抓取网页。我知道 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()
但是如果我在 运行ning 时按 'Ctrl+C',可能会出现一些奇怪的错误。我的意思是有时 'Ctrl+C' 可以正常关闭程序。没有错误信息。然而,在某些情况下,程序在按下 'Ctrl+C' 后仍会 运行ning 并且在所有工作完成之前不会停止。如果我在那一刻按'Ctrl+C','Task was destroyed but it is pending!'就会在那里。
我已经阅读了一些关于 asyncio 的主题,并在 main() 中添加了一些代码以优雅地关闭协程。但它不起作用。有没有人遇到类似的问题?
我打赌问题出在这里:
except:
pass
你should never do这种东西。你的情况是另一种可能发生的情况的例子。
当您取消任务并等待其取消时,asyncio.CancelledError
在任务内部引发,
这就是你这样做的原因
task.cancel()
with suppress(asyncio.CancelledError):
loop.run_until_complete(task) # this line should raise CancelledError,
# otherwise task will continue
实际取消任务。
更新:
But I still hardly understand why the original code could quit well by 'Ctrl+C' at a uncertain probability?
它依赖于你的任务状态:
- 如果此时你按下'Ctrl+C'所有任务都完成了,没有
他们将在等待时提高
CancelledError
,您的代码将正常完成。 - 如果在您按下 'Ctrl+C' 时某些任务处于待处理状态,但接近于完成它们的执行,您的代码将在任务取消时卡住一点,并在任务完成后不久完成。
- 如果此时您按下 'Ctrl+C' 某些任务正在等待处理并且 远未完成,您的代码将在尝试取消这些任务时卡住(这 无法完成)。另一个 'Ctrl+C' 将中断进程 取消,但任务不会被取消或完成然后你会得到 警告 'Task was destroyed but it is pending!'.
我假设您正在使用任何风格的 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
对它们执行硬杀。但是,这不会给他们任何反应的选择,并且可能会导致问题。