导入scipy.stats后,Ctrl-C使Python崩溃
我在64位的Windows 7上运行64位的Python 2.7.3。我可以通过以下方式让Python解释器崩溃:
>>> from scipy import stats
>>> import time
>>> time.sleep(3)
然后在程序暂停的时候按下Control-C。结果是没有出现键盘中断的提示,解释器直接崩溃了。屏幕上会显示以下内容:
forrtl: error (200): program aborting due to control-C event
Image PC Routine Line Source
libifcoremd.dll 00000000045031F8 Unknown Unknown Unknown
libifcoremd.dll 00000000044FC789 Unknown Unknown Unknown
libifcoremd.dll 00000000044E8583 Unknown Unknown Unknown
libifcoremd.dll 000000000445725D Unknown Unknown Unknown
libifcoremd.dll 00000000044672A6 Unknown Unknown Unknown
kernel32.dll 0000000077B74AF3 Unknown Unknown Unknown
kernel32.dll 0000000077B3F56D Unknown Unknown Unknown
ntdll.dll 0000000077C73281 Unknown Unknown Unknown
这让长时间运行的scipy计算无法被中断。
我在网上搜索“forrtl”等相关内容,发现有人说这个问题可能是因为使用了一个Fortran库,它改变了Ctrl-C的处理方式。不过在Scipy的错误追踪系统上没有看到相关的bug,但考虑到Scipy是用来和Python一起使用的,我觉得这应该算是个bug。因为它破坏了Python对Ctrl-C的处理。有办法解决这个问题吗?
编辑:根据@cgohlke的建议,我在导入scipy后尝试添加自己的处理程序。这个问题讨论了一个相关的问题,显示添加信号处理程序并没有效果。我尝试通过pywin32使用Windows API的SetConsoleCtrlHandler函数:
from scipy import stats
import win32api
def doSaneThing(sig, func=None):
print "Here I am"
raise KeyboardInterrupt
win32api.SetConsoleCtrlHandler(doSaneThing, 1)
这样做之后,按下Ctrl-C会打印“Here I am”,但Python仍然因为forrtl错误而崩溃。有时候我还会看到一个消息说“ConsoleCtrlHandler函数失败”,但这个消息很快就消失了。
如果我在IPython中运行这个,我可以在forrtl错误之前看到正常的Python键盘中断追踪信息。如果我引发其他错误而不是键盘中断(比如ValueError),我也能看到正常的Python追踪信息,后面跟着forrtl错误:
ValueError Traceback (most recent call last)
<ipython-input-1-08defde66fcb> in doSaneThing(sig, func)
3 def doSaneThing(sig, func=None):
4 print "Here I am"
----> 5 raise ValueError
6 win32api.SetConsoleCtrlHandler(doSaneThing, 1)
ValueError:
forrtl: error (200): program aborting due to control-C event
[etc.]
看起来无论底层的处理程序在做什么,它并不是直接捕捉Ctrl-C,而是对错误情况(ValueError)做出了反应,导致自己崩溃。有没有办法解决这个问题呢?
7 个回答
我找到了一种半解决办法,方法是这样做:
from scipy import stats
import win32api
def doSaneThing(sig, func=None):
return True
win32api.SetConsoleCtrlHandler(doSaneThing, 1)
在处理程序中返回true可以停止后续的处理程序,这样就不会再调用那个干扰的Fortran处理程序了。不过,这个解决办法并不完全,主要有两个原因:
- 它实际上并没有引发KeyboardInterrupt,这意味着我无法在Python代码中对此做出反应。它只是让我回到了命令提示符。
- 它并没有像Ctrl-C在Python中那样完全中断程序。如果在一个新的Python会话中,我执行
time.sleep(3)
然后按下Ctrl-C,程序会立即停止休眠并引发KeyboardInterrupt。而使用上面的解决办法,休眠不会被中断,只有等到休眠时间结束后,控制权才会返回到命令提示符。
尽管如此,这总比整个会话崩溃要好。对我来说,这引出了一个问题:为什么SciPy(以及其他依赖这些Intel库的Python库)不自己处理这个问题。
我把这个回答留作未接受,希望有人能提供一个真正的解决方案或替代办法。这里的“真正”是指在长时间运行的SciPy计算中按下Ctrl-C应该和没有加载SciPy时的效果一样。(注意,这并不意味着它必须立即生效。像普通Python的sum(xrange(100000000))
这样的非SciPy计算在按下Ctrl-C时可能不会立即中断,但至少当它们中断时,会引发KeyboardInterrupt。)
把环境变量 FOR_DISABLE_CONSOLE_CTRL_HANDLER
设置为 1
似乎可以解决这个问题,但前提是必须在加载有问题的包之前就设置好这个变量。
import os
os.environ['FOR_DISABLE_CONSOLE_CTRL_HANDLER'] = '1'
[...]
编辑: 虽然按 Ctrl+C 不再让 Python 崩溃了,但它也无法停止当前的计算。
这里有一个你发布的解决方案的变种,可能会有效。也许还有更好的方法来解决这个问题,或者甚至可以通过设置一个环境变量来避免这个问题,这样DLL就会跳过安装处理程序。希望这能帮到你,直到你找到更好的办法。
在 time
模块(第868-876行)和 _multiprocessing
模块(第312-321行)中,都调用了 SetConsoleCtrlHandler
。在 time
模块的情况下,它的控制处理程序设置了一个Windows事件 hInterruptEvent
。对于主线程来说,time.sleep
通过 WaitForSingleObject(hInterruptEvent, ul_millis)
等待这个事件,其中 ul_millis
是要睡眠的毫秒数,除非被 Ctrl+C 中断。由于你安装的处理程序返回 True
,所以 time
模块的处理程序从未被调用来设置 hInterruptEvent
,这意味着 sleep
无法被中断。
我尝试使用 imp.init_builtin('time')
来重新初始化 time
模块,但显然 SetConsoleCtrlHandler
忽略了第二次调用。看起来处理程序必须先被移除,然后再重新插入。不幸的是,time
模块并没有提供这样的函数。因此,作为一种权宜之计,只需确保在安装处理程序后再导入 time
模块。由于导入 scipy
也会导入 time
,你需要使用 ctypes
预加载 libifcoremd.dll,以确保处理程序的顺序正确。最后,添加一个对 thread.interrupt_main
的调用,以确保Python的 SIGINT
处理程序被调用[1]。
例如:
import os
import imp
import ctypes
import thread
import win32api
# Load the DLL manually to ensure its handler gets
# set before our handler.
basepath = imp.find_module('numpy')[1]
ctypes.CDLL(os.path.join(basepath, 'core', 'libmmd.dll'))
ctypes.CDLL(os.path.join(basepath, 'core', 'libifcoremd.dll'))
# Now set our handler for CTRL_C_EVENT. Other control event
# types will chain to the next handler.
def handler(dwCtrlType, hook_sigint=thread.interrupt_main):
if dwCtrlType == 0: # CTRL_C_EVENT
hook_sigint()
return 1 # don't chain to the next handler
return 0 # chain to the next handler
win32api.SetConsoleCtrlHandler(handler, 1)
>>> import time
>>> from scipy import stats
>>> time.sleep(10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyboardInterrupt
[1] interrupt_main
调用 PyErr_SetInterrupt
。这会触发 Handlers[SIGINT]
,并调用 Py_AddPendingCall
来添加 checksignals_witharg
。接着,这会调用 PyErr_CheckSignals
。由于 Handlers[SIGINT]
被触发,这会调用 Handlers[SIGINT].func
。最后,如果 func
是 signal.default_int_handler
,你将会得到一个 KeyboardInterrupt
异常。