我正在Windows上编写一个多线程Python应用程序。
我曾经使用ctrl-c
终止该应用,但是一旦我添加threading.Timer
个实例ctrl-c
停止工作(或者有时需要很长时间)。
怎么会这样?
定时器线程与ctrl-c
之间的关系是什么?
更新
我在Python的thread documentation中找到了以下内容:
线程奇怪地与之交互 中断:KeyboardInterrupt 将收到例外情况 任意线程。 (当信号 模块可用,始终中断 转到主线程。)
答案 0 :(得分:4)
threading.Thread
(以及threading.Timer
)的工作方式是每个线程向threading
模块注册自己,并在解释器退出时,解释器将等待所有注册的线程退出之前正确终止翻译。这样做是为了让线程实际完成执行,而不是将解释器从它们下面粗暴地删除。所以当你点击^ C时,主线程收到信号,决定终止并等待定时器完成。
您可以设置线程 daemonic (使用setDaemon
方法)以使线程模块不等待这些线程,但如果它们恰好在解释器退出时执行Python代码,退出时会出现混乱的错误。即使您取消threading.Timer
(并设置它的守护程序),它仍然可以在解释器被销毁时唤醒 - 因为threading.Timer
的{{1}}方法只是告诉{{1}它在唤醒时不执行任何操作,但它必须实际执行Python代码才能做出决定。
没有优雅的方法来终止线程(当前的线程除外),也没有可靠的方法来中断被阻塞的线程。对定时器更易于管理的方法通常是事件循环,就像GUI和其他事件驱动系统为您提供的那样。使用什么完全取决于你的程序将要做什么。
答案 1 :(得分:2)
David Beazley的演讲揭示了这个话题。 PDF可用here。查看第22--25页(“Interlude:Signals”到“Frozen Signals”)。
答案 2 :(得分:0)
这是一种可能的解决方法:使用 time.sleep()
而不是 Timer
意味着可以实现“正常关闭”机制......对于 Python3,它似乎只引发了 KeyboardInterrupt
在主线程的用户代码中。否则,它似乎按照 here 将异常“忽略”:实际上,它会导致发生它的线程立即死亡,但不会导致无法捕获它的任何祖先线程。
假设您希望 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
只能在主线程上被捕获。
PS 根据我的实验,处理子进程可能更容易,因为至少在一个简单的情况下,Ctrl-C 似乎会导致 KeyboardInterrupt
在所有正在运行的进程中同时发生。
答案 3 :(得分:0)
将你的主要 while 循环包裹在一个 try 中,除了:
from threading import Timer
import time
def randomfn():
print ("Heartbeat sent!")
class RepeatingTimer(Timer):
def run(self):
while not self.finished.is_set():
self.function(*self.args, **self.kwargs)
self.finished.wait(self.interval)
t = RepeatingTimer(10.0, function=randomfn)
print ("Starting...")
t.start()
while (True):
try:
print ("Hello")
time.sleep(1)
except:
print ("Cancelled timer...")
t.cancel()
print ("Cancelled loop...")
break
print ("End")
结果:
Heartbeat sent!
Hello
Hello
Hello
Hello
Hello
Hello
Hello
Hello
Hello
Cancelled timer...
Cancelled loop...
End