使用threading.Timer时Ctrl-C无效
我正在Windows上写一个多线程的Python应用。
我以前是通过按ctrl-c
来结束这个应用,但自从我添加了threading.Timer
实例后,ctrl-c
就不太管用了(有时候还需要很长时间才有效)。
这是怎么回事呢?
定时器线程和ctrl-c
有什么关系呢?
更新:
我在Python的线程文档中发现了以下内容:
线程与中断的互动很奇怪:
KeyboardInterrupt异常会被任意线程接收。(当信号模块可用时,中断总是发送到主线程。)
4 个回答
这里有一个可能的解决办法:用 time.sleep()
来代替 Timer
,这样就可以实现一个“优雅关闭”的机制……这是针对 Python3 的,因为在这个版本中,似乎 KeyboardInterrupt
只会在主线程的用户代码中被触发。否则,似乎这个异常会被“忽略”,就像在这里提到的那样:实际上,它会导致发生异常的线程立即结束,但不会影响到它的父线程,这样就很麻烦,因为父线程无法捕捉到这个异常。
假设你希望 Ctrl-C 的响应时间是 0.5 秒,但你只想每 5 秒重复一些实际的工作(工作持续时间是随机的,如下所示):
import threading, sys, time, random
blip_counter = 0
work_threads=[]
def repeat_every_5():
global blip_counter
print( f'counter: {blip_counter}')
def real_work():
real_work_duration_s = random.randrange(10)
print( f'do some real work every 5 seconds, lasting {real_work_duration_s} s: starting...')
# in a real world situation stop_event.is_set() can be tested anywhere in the code
for interval_500ms in range( real_work_duration_s * 2 ):
if threading.current_thread().stop_event.is_set():
print( f'stop_event SET!')
return
time.sleep(0.5)
print( f'...real work ends')
# clean up work_threads as appropriate
for work_thread in work_threads:
if not work_thread.is_alive():
print(f'work thread {work_thread} dead, removing from list' )
work_threads.remove( work_thread )
new_work_thread = threading.Thread(target=real_work)
# stop event for graceful shutdown
new_work_thread.stop_event = threading.Event()
work_threads.append(new_work_thread)
# in fact, because a graceful shutdown is now implemented, new_work_thread doesn't have to be daemon
# new_work_thread.daemon = True
new_work_thread.start()
blip_counter += 1
time.sleep( 5 )
timer_thread = threading.Thread(target=repeat_every_5)
timer_thread.daemon = True
timer_thread.start()
repeat_every_5()
while True:
try:
time.sleep( 0.5 )
except KeyboardInterrupt:
print( f'shutting down due to Ctrl-C..., work threads left: {len(work_threads)}')
# trigger stop event for graceful shutdown
for work_thread in work_threads:
if work_thread.is_alive():
print( f'work_thread {work_thread}: setting STOP event')
work_thread.stop_event.set()
print( f'work_thread {work_thread}: joining to main...')
work_thread.join()
print( f'work_thread {work_thread}: ...joined to main')
else:
print( f'work_thread {work_thread} has died' )
sys.exit(1)
这个 while True:
的机制看起来有点笨重。不过,我认为,正如我所说的,目前(Python 3.8.x) KeyboardInterrupt
只能在主线程中捕捉到。
另外,根据我的实验,处理子进程可能会更简单,因为在简单的情况下,似乎 Ctrl-C 会导致所有运行中的进程同时发生 KeyboardInterrupt
。
David Beazley 有一个演讲,讲解了这个话题的一些内容。你可以在这里找到这个 PDF 文件:这里。可以查看第 22 到 25 页的内容,标题是“插曲:信号”到“冻结信号”。
threading.Thread
(还有threading.Timer
)的工作方式是,每个线程会向threading
模块注册自己。当解释器要退出时,它会等所有注册的线程都结束后,才会真正关闭自己。这样做是为了确保线程能够完成它们的工作,而不是让解释器直接把它们“踢出去”。所以当你按下^C时,主线程会收到这个信号,决定要结束,然后会等计时器完成。
你可以把线程设置为守护线程(使用setDaemon
方法),这样threading
模块就不会等这些线程了。但是如果这些守护线程在解释器退出时正在执行Python代码,就会出现一些让人困惑的错误。即使你取消了threading.Timer
(并把它设置为守护线程),它在解释器销毁的时候仍然可能会被唤醒——因为threading.Timer
的cancel
方法只是告诉它醒来时不要执行任何东西,但它必须实际执行Python代码才能做出这个决定。
没有优雅的方法可以终止线程(除了当前线程),也没有可靠的方法可以中断一个被阻塞的线程。通常,更好管理的计时器方法是使用事件循环,就像图形用户界面(GUI)和其他事件驱动系统提供的那样。具体使用什么,完全取决于你的程序还要做些什么。