python 2.6.x 线程/信号/atexit 在某些版本中失败?
我看到很多人问这个问题……但是我的代码在 Python 2.6.2 上 能正常工作,而在 Python 2.6.5 上却 不行。我是不是想错了?我以为 atexit 模块的“通过这个模块注册的函数在程序被信号终止时不会被调用”这个说法在这里不适用,因为我捕获了信号,然后正常退出?这是怎么回事?我该怎么做才对呢?
import atexit, sys, signal, time, threading
terminate = False
threads = []
def test_loop():
while True:
if terminate:
print('stopping thread')
break
else:
print('looping')
time.sleep(1)
@atexit.register
def shutdown():
global terminate
print('shutdown detected')
terminate = True
for thread in threads:
thread.join()
def close_handler(signum, frame):
print('caught signal')
sys.exit(0)
def run():
global threads
thread = threading.Thread(target=test_loop)
thread.start()
threads.append(thread)
while True:
time.sleep(2)
print('main')
signal.signal(signal.SIGINT, close_handler)
if __name__ == "__main__":
run()
Python 2.6.2:
$ python halp.py
looping
looping
looping
main
looping
main
looping
looping
looping
main
looping
^Ccaught signal
shutdown detected
stopping thread
Python 2.6.5:
$ python halp.py
looping
looping
looping
main
looping
looping
main
looping
looping
main
^Ccaught signal
looping
looping
looping
looping
...
looping
looping
Killed <- kill -9 process at this point
在 2.6.5 版本中,主线程似乎从来没有执行 atexit 函数。
3 个回答
我不太确定这个是否完全变了,但这是我在2.6.5版本中设置atexit的方法。
atexit.register(goodbye)
def goodbye():
print "\nStopping..."
因为信号而退出程序和在信号处理器里退出是两回事。捕捉到信号后用 sys.exit 退出是干净的退出方式,而不是因为信号处理器而退出。所以,我同意在这里应该运行 atexit 处理器——至少从原则上讲是这样。
不过,信号处理器有个棘手的地方:它们是完全异步的。也就是说,它们可以在任何时候打断程序的执行,甚至是在执行任何指令的时候。比如说这段代码。(把它当作和你上面的代码一样的形式;为了简洁我省略了一些代码。)
import threading
lock = threading.Lock()
def test_loop():
while not terminate:
print('looping')
with lock:
print "Executing synchronized operation"
time.sleep(1)
print('stopping thread')
def run():
while True:
time.sleep(2)
with lock:
print "Executing another synchronized operation"
print('main')
这里有个严重的问题:当 run() 函数持有 lock
时,可能会收到一个信号(比如按下 ^C)。如果发生这种情况,你的信号处理器会在仍然持有锁的情况下运行。然后它会等待 test_loop 退出,如果那个线程在等待锁,你就会出现死锁的情况。
这就是一类问题,这也是为什么很多 API 不建议在信号处理器中调用它们的原因。相反,你应该设置一个标志,告诉主线程在合适的时候关闭。
do_shutdown = False
def close_handler(signum, frame):
global do_shutdown
do_shutdown = True
print('caught signal')
def run():
while not do_shutdown:
...
我个人的偏好是完全避免用 sys.exit 退出程序,而是在主退出点(比如 run() 的结束)明确进行清理,但如果你想的话,这里可以使用 atexit。
这里的根本区别其实和信号以及atexit没有关系,而是和sys.exit
的行为变化有关。
在大约2.6.5版本之前,sys.exit
(更准确地说,是在顶层捕获SystemExit)会导致解释器退出;如果还有线程在运行,它们会被终止,就像POSIX线程一样。
而在2.6.5版本左右,这个行为发生了变化:现在sys.exit
的效果基本上和程序的主函数返回是一样的。当你这样做的时候——在两个版本中——解释器会等所有线程结束后再退出。
相关的变化是,Py_Finalize
现在在最开始就调用了wait_for_thread_shutdown()
,而之前并没有这样做。
这个行为变化似乎是不正确的,主要是因为它不再按照文档中的说明工作,文档上简单地写着:“从Python退出。” 实际效果不再是从Python退出,而只是退出线程。(顺便提一下,sys.exit
在另一个线程中调用时从来没有退出Python,但这种与文档行为的偏差并不能为更大的偏差辩护。)
我能理解新行为的吸引力:与其有两种方式退出主线程(“退出并等待线程”和“立即退出”),不如只有一种方式,因为sys.exit
本质上和从顶层函数返回是一样的。然而,这个变化是破坏性的,并且偏离了文档中的行为,这一点远比它的好处要重要。
由于这个变化,在上面的信号处理程序中调用sys.exit
后,解释器会等待线程退出,然后再运行atexit
处理程序。由于是处理程序本身告诉线程退出,结果就导致了死锁。