使用threading.Timer时Ctrl-C无效

9 投票
4 回答
5326 浏览
提问于 2025-04-16 20:56

我正在Windows上写一个多线程的Python应用。

我以前是通过按ctrl-c来结束这个应用,但自从我添加了threading.Timer实例后,ctrl-c就不太管用了(有时候还需要很长时间才有效)。

这是怎么回事呢?
定时器线程和ctrl-c有什么关系呢?

更新:
我在Python的线程文档中发现了以下内容:

线程与中断的互动很奇怪:
KeyboardInterrupt异常会被任意线程接收。(当信号模块可用时,中断总是发送到主线程。)

4 个回答

0

这里有一个可能的解决办法:用 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

2

David Beazley 有一个演讲,讲解了这个话题的一些内容。你可以在这里找到这个 PDF 文件:这里。可以查看第 22 到 25 页的内容,标题是“插曲:信号”到“冻结信号”。

7

threading.Thread(还有threading.Timer)的工作方式是,每个线程会向threading模块注册自己。当解释器要退出时,它会等所有注册的线程都结束后,才会真正关闭自己。这样做是为了确保线程能够完成它们的工作,而不是让解释器直接把它们“踢出去”。所以当你按下^C时,主线程会收到这个信号,决定要结束,然后会等计时器完成。

你可以把线程设置为守护线程(使用setDaemon方法),这样threading模块就不会等这些线程了。但是如果这些守护线程在解释器退出时正在执行Python代码,就会出现一些让人困惑的错误。即使你取消了threading.Timer(并把它设置为守护线程),它在解释器销毁的时候仍然可能会被唤醒——因为threading.Timercancel方法只是告诉它醒来时不要执行任何东西,但它必须实际执行Python代码才能做出这个决定。

没有优雅的方法可以终止线程(除了当前线程),也没有可靠的方法可以中断一个被阻塞的线程。通常,更好管理的计时器方法是使用事件循环,就像图形用户界面(GUI)和其他事件驱动系统提供的那样。具体使用什么,完全取决于你的程序还要做些什么。

撰写回答