我在使用简单的多线程Python循环程序时遇到麻烦。它应该无限循环并以 Ctrl + C 停止。这是使用threading
的实现:
from threading import Thread, Event
from time import sleep
stop = Event()
def loop():
while not stop.is_set():
print("looping")
sleep(2)
try:
thread = Thread(target=loop)
thread.start()
thread.join()
except KeyboardInterrupt:
print("stopping")
stop.set()
此MWE是从更复杂的代码中提取的(显然,我 不需要多线程来创建无限循环)。
它可以在Linux上正常运行,但不能在Windows上运行: Ctrl + C 事件没有被拦截,并且循环无限地继续。根据{{3}},不同的行为归因于两个操作系统处理 Ctrl + C 的方式。
因此,似乎不能简单地在Windows上依靠threading
的 Ctrl + C 。我的问题是:通过 Ctrl + C 可以在此OS上停止多线程Python脚本的其他方法是什么?
答案 0 :(得分:2)
正如Nathaniel J. Smith在问题链接中所解释的那样,至少从CPython 3.7开始,Ctrl-C无法唤醒Windows上的主线程:
最终结果是,在Windows上,control-C几乎永远无法工作 唤醒阻塞的Python进程,但有一些特殊的例外,其中 有人做了工作来实现这一目标。在Python 2上唯一的功能 实现此功能的是time.sleep()和 multiprocessing.Semaphore.acquire;在Python 3上还有更多 (您可以grep _PyOS_SigintEvent的源以找到它们),但是 Thread.join不是其中之一。
那么,你能做什么?
一种选择是不使用Ctrl-C杀死程序,而使用诸如TerminateProcess
之类的调用对象,例如内置taskkill
工具或使用os
模块。但是你不想要那样。
显然,等到他们提出Python 3.8或3.9中的修复程序,或者永远不要在无法进行Ctrl-C程序之前,永远不能接受。
因此,您唯一可以做的就是不阻塞Thread.join
上的主线程或其他任何不可中断的东西。
一种快速而又肮脏的解决方案是仅对join
进行超时轮询:
while thread.is_alive():
thread.join(0.2)
现在,您的程序在执行while
循环并调用is_alive
时会短暂地被中断,然后再返回200ms的不间断睡眠。在这200毫秒内出现的所有Ctrl-C都将等待您对其进行处理,所以这不是问题。
除了200毫秒已经足够长,足以引起注意甚至令人讨厌。
它可能太短也可能太长。当然,每200ms唤醒并执行少量Python字节码并不会浪费太多的CPU,但这不是 nothing ,它仍然在调度程序中获得了时间片,这可能足以满足例如,避免笔记本电脑进入其长期低功耗模式之一。
clean 解决方案是找到另一个要阻止的函数。正如纳撒尼尔·史密斯(Nathaniel J. Smith)所说:
您可以grep _PyOS_SigintEvent的源以找到它们
但是可能没有什么合适的。很难想象您将如何设计程序来阻塞multiprocessing.Semaphore.acquire
,而不会给读者造成混乱……
在这种情况下,您可能想通过PyWin32
或ctypes
直接拖动Win32 API。看一下time.sleep
和multiprocessing.Semaphore.acquire
之类的函数如何可中断,阻塞它们正在使用的内容以及使线程发出信号,无论您在退出时阻塞什么。
如果您愿意使用CPython的未记录内部结构,则至少在3.7中,隐藏的_winapi
模块在a wrapper function周围有WaitForMultipleObjects
,并附加了魔法{{ 1}},当您执行等待优先而不是全部等待时。
您可以传递给_PyOSSigintEvent
的东西之一是Win32线程句柄,它与WaitForMultipleObjects
的作用相同,尽管我不确定是否有简单的方法来获取线程处理Python线程之外的内容。
或者,您可以手动创建某种内核同步对象(我不太了解join
模块,而且我没有Windows系统,因此您可能必须阅读源自己,或者至少在交互式解释器中_winapi
来获取它,以查看其提供的包装器),help
,并让线程发出信号。