在 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+C 和 threading
在 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.sleep
和 multiprocessing.Semaphore.acquire
这样的函数如何设法成为可中断的,阻塞它们正在使用的任何东西,并让你的线程发出信号,无论你在退出时阻塞什么。
如果您愿意使用未记录的 CPython 内部结构,看起来至少在 3.7 中,隐藏的 _winapi
模块具有 a wrapper function around WaitForMultipleObjects
附加魔法 _PyOSSigintEvent
当你先等待而不是等待所有时为你。
你可以传递给 WaitForMultipleObjects
的东西之一是 Win32 线程句柄,它与 join
具有相同的效果,尽管我不确定是否有简单的方法来获取Python 线程的线程句柄。
或者,您可以手动创建某种内核同步对象(我不太了解 _winapi
模块,而且我没有 Windows 系统,所以您'您可能必须自己阅读源代码,或者至少 help
在交互式解释器中阅读源代码,以查看它提供的包装器),WaitForMultipleObjects
然后让线程发出信号。
我在使用简单的多线程 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+C 和 threading
在 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.sleep
和 multiprocessing.Semaphore.acquire
这样的函数如何设法成为可中断的,阻塞它们正在使用的任何东西,并让你的线程发出信号,无论你在退出时阻塞什么。
如果您愿意使用未记录的 CPython 内部结构,看起来至少在 3.7 中,隐藏的 _winapi
模块具有 a wrapper function around WaitForMultipleObjects
附加魔法 _PyOSSigintEvent
当你先等待而不是等待所有时为你。
你可以传递给 WaitForMultipleObjects
的东西之一是 Win32 线程句柄,它与 join
具有相同的效果,尽管我不确定是否有简单的方法来获取Python 线程的线程句柄。
或者,您可以手动创建某种内核同步对象(我不太了解 _winapi
模块,而且我没有 Windows 系统,所以您'您可能必须自己阅读源代码,或者至少 help
在交互式解释器中阅读源代码,以查看它提供的包装器),WaitForMultipleObjects
然后让线程发出信号。