使用 threading.Timer 时,按 Ctrl-C 无法正常工作

9
我正在Windows上编写一个多线程的Python应用程序。
我曾经使用ctrl-c来终止应用程序,但是一旦我添加了threading.Timer实例,ctrl-c就停止工作了(或者有时需要很长时间才能起作用)。
这是怎么回事?有Timer线程和ctrl-c之间的关系吗?
更新: 我在Python的线程文档中找到了以下内容:
线程与中断的交互方式很奇怪:KeyboardInterrupt异常将被任意线程接收。(当信号模块可用时,中断总是进入主线程。)

你能否在这里粘贴代码? - Rahul
4个回答

7
threading.Thread(因此threading.Timer)的工作方式是,每个线程都向threading模块注册自己,在解释器退出时,解释器将等待所有已注册的线程退出后才终止解释器本身。这样做是为了使线程实际完成执行,而不是让解释器在它们下面被残酷地移除。因此,当您按下^C时,主线程接收到信号,决定终止并等待计时器完成。
您可以通过使用setDaemon方法将线程设置为守护进程,以使线程模块不等待这些线程,但如果它们在解释器退出时恰好正在执行Python代码,则会在退出期间出现混乱的错误。即使您取消了threading.Timer(并将其设置为守护进程),它仍然可能在解释器被销毁时唤醒--因为threading.Timercancel方法只是告诉threading.Timer在唤醒时不执行任何操作,但它必须实际执行Python代码才能进行该确定。
没有优雅的方法来终止线程(除了当前线程),也没有可靠的方法来中断被阻塞的线程。通常,处理定时器的更可管理的方法是事件循环,例如GUI和其他事件驱动系统提供的方法。使用什么取决于您的程序将执行什么样的操作。

感谢您提供更大的视角。目前,我将线程标记为守护进程,解决了问题,但我想在某个时候我需要重构以实现正确的终止流程。 - Jonathan Livni
如果在同一线程中设置定时器,信号处理程序会被执行吗?如果可以在信号处理程序中设置timer.Cancel,那么进程现在可以退出吗? - Vivek
+1. 有没有一些资源可以解释为什么除了当前线程之外,没有优雅的方式来终止线程,也没有可靠的方法来中断被阻塞的线程? - n611x007
请看我在这里的回答。将threading.Event附加到threading.Thread是完全可行的,并且似乎可以实现“优雅关闭”。或者我的解决方案存在根本性问题吗? - mike rodent

2

这里有一份David Beazley的演示文稿,可以为这个话题提供一些启示。PDF文件可以在这里获取。请查看22-25页(从“插曲:信号”到“冻结信号”)。


这确实是一份富有洞见的PDF! - Jonathan Livni

0

将主要的 while 循环放在 try except 语句块中:

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

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接