在Windows上停止多线程Python脚本

时间:2018-08-21 18:22:07

标签: python windows multithreading python-multithreading

我在使用简单的多线程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脚本的其他方法是什么?

1 个答案:

答案 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,而不会给读者造成混乱……

在这种情况下,您可能想通过PyWin32ctypes直接拖动Win32 API。看一下time.sleepmultiprocessing.Semaphore.acquire之类的函数如何可中断,阻塞它们正在使用的内容以及使线程发出信号,无论您在退出时阻塞什么。


如果您愿意使用CPython的未记录内部结构,则至少在3.7中,隐藏的_winapi模块在​​a wrapper function周围有WaitForMultipleObjects,并附加了魔法{{ 1}},当您执行等待优先而不是全部等待时。

您可以传递给_PyOSSigintEvent的东西之一是Win32线程句柄,它与WaitForMultipleObjects的作用相同,尽管我不确定是否有简单的方法来获取线程处理Python线程之外的内容。

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