在 Windows 上停止多线程 Python 脚本

Stop multithreaded Python script on Windows

我在使用简单的多线程 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 事件没有被拦截,循环无限继续。根据 Python Dev mailing list,不同的行为是由于 Ctrl+C 由两个 OS 处理的方式s.

所以,看来不能简单地依靠 Ctrl+Cthreading 在 Windows .我的问题是:使用 Ctrl+C 在这个 OS 上停止多线程 Python 脚本的其他方法是什么?

正如 Nathaniel J. Smith 在你的问题 link 中所解释的那样,至少从 CPython 3.7 开始,Ctrl-C 无法在 Windows 上唤醒你的主线程:

The end result is that on Windows, control-C almost never works to wake up a blocked Python process, with a few special exceptions where someone did the work to implement this. On Python 2 the only functions that have this implemented are time.sleep() and multiprocessing.Semaphore.acquire; on Python 3 there are a few more (you can grep the source for _PyOS_SigintEvent to find them), but Thread.join isn't one of them.

那么,你能做什么?


一个选择是不使用 Ctrl-C 来终止您的程序,而是使用调用的东西,例如 TerminateProcess,例如内置的 taskkill 工具,或 Python 脚本使用 os 模块。但你不想要那个。

很明显,等到他们在 Python 3.8 或 3.9 中提出修复,或者在您可以 Ctrl-C 之前,您的程序是不可接受的。

所以,你唯一能做的就是不要在 Thread.join 上阻塞主线程,或者其他任何不可中断的事情。


快速而肮脏的解决方案是只轮询 join 超时:

while thread.is_alive():
    thread.join(0.2)

现在,您的程序在执行 while 循环和调用 is_alive 时可短暂中断,然后再返回不可中断睡眠 200 毫秒。在这 200 毫秒内出现的任何 Ctrl-C 都会等待您处理它,所以这不是问题。

除了 200 毫秒已经足够长以致于引人注目并且可能令人讨厌。

它可能太短也可能太长。当然,每 200 毫秒唤醒并执行少量 Python 字节码并没有浪费太多 CPU,但它并不是 nothing,而且它仍然在获取时间片调度程序,这可能足以,例如,防止笔记本电脑进入其长期低功耗模式之一。


clean 解决方案是找到另一个要阻塞的函数。正如 Nathaniel J. Smith 所说:

you can grep the source for _PyOS_SigintEvent to find them

但可能没有非常合身的。很难想象你会如何设计你的程序来阻止 multiprocessing.Semaphore.acquire 并且不会让 reader…

感到非常困惑

在那种情况下,您可能想直接拖入 Win32 API,无论是通过 PyWin32 还是 ctypes。看看像 time.sleepmultiprocessing.Semaphore.acquire 这样的函数如何设法成为可中断的,阻塞它们正在使用的任何东西,并让你的线程发出信号,无论你在退出时阻塞什么。


如果您愿意使用未记录的 CPython 内部结构,看起来至少在 3.7 中,隐藏的 _winapi 模块具有 a wrapper function around WaitForMultipleObjects 附加魔法 _PyOSSigintEvent 当你先等待而不是等待所有时为你。

你可以传递给 WaitForMultipleObjects 的东西之一是 Win32 线程句柄,它与 join 具有相同的效果,尽管我不确定是否有简单的方法来获取Python 线程的线程句柄。

或者,您可以手动创建某种内核同步对象(我不太了解 _winapi 模块,而且我没有 Windows 系统,所以您'您可能必须自己阅读源代码,或者至少 help 在交互式解释器中阅读源代码,以查看它提供的包装器),WaitForMultipleObjects 然后让线程发出信号。