【问题标题】:Script stuck on exit when using atexit to terminate threads使用 atexit 终止线程时脚本卡在退出时
【发布时间】:2019-11-18 08:15:33
【问题描述】:

我正在使用 python 3.7.4 上的线程,我想使用 atexit 注册一个将(干净地)终止线程的清理函数。

例如:

# example.py
import threading
import queue
import atexit
import sys

Terminate = object()

class Worker(threading.Thread):
    def __init__(self):
        super().__init__()
        self.queue = queue.Queue()

    def send_message(self, m):
        self.queue.put_nowait(m)

    def run(self):
        while True:
            m = self.queue.get()
            if m is Terminate:
                break
            else:
                print("Received message: ", m)


def shutdown_threads(threads):
    for t in threads:
        print(f"Terminating thread {t}")
        t.send_message(Terminate)
    for t in threads:
        print(f"Joining on thread {t}")
        t.join()
    else:
        print("All threads terminated")

if __name__ == "__main__":
    threads = [
        Worker()
        for _ in range(5)
    ]
    atexit.register(shutdown_threads, threads)

    for t in threads:
        t.start()

    for t in threads:
        t.send_message("Hello")
        #t.send_message(Terminate)

    sys.exit(0)

但是,似乎与 atexit 回调中的线程和队列交互会通过一些内部关闭例程创建死锁:

$ python example.py
Received message:  Hello
Received message:  Hello
Received message:  Hello
Received message:  Hello
Received message:  Hello
^CException ignored in: <module 'threading' from '/usr/lib64/python3.7/threading.py'>
Traceback (most recent call last):
  File "/usr/lib64/python3.7/threading.py", line 1308, in _shutdown
    lock.acquire()
KeyboardInterrupt
Terminating thread <Worker(Thread-1, started 140612492904192)>
Terminating thread <Worker(Thread-2, started 140612484511488)>
Terminating thread <Worker(Thread-3, started 140612476118784)>
Terminating thread <Worker(Thread-4, started 140612263212800)>
Terminating thread <Worker(Thread-5, started 140612254820096)>
Joining on thread <Worker(Thread-1, stopped 140612492904192)>
Joining on thread <Worker(Thread-2, stopped 140612484511488)>
Joining on thread <Worker(Thread-3, stopped 140612476118784)>
Joining on thread <Worker(Thread-4, stopped 140612263212800)>
Joining on thread <Worker(Thread-5, stopped 140612254820096)>
All threads terminated

KeyboardInterrupt 是我使用的ctrl-c,因为该过程似乎无限期挂起)。

但是,如果我在退出之前发送Terminate 消息(取消注释t.send_message("Hello") 之后的行),程序不会挂起并正常终止:

$ python example.py
Received message:  Hello
Received message:  Hello
Received message:  Hello
Received message:  Hello
Received message:  Hello
Terminating thread <Worker(Thread-1, stopped 140516051592960)>
Terminating thread <Worker(Thread-2, stopped 140516043200256)>
Terminating thread <Worker(Thread-3, stopped 140515961992960)>
Terminating thread <Worker(Thread-4, stopped 140515953600256)>
Terminating thread <Worker(Thread-5, stopped 140515945207552)>
Joining on thread <Worker(Thread-1, stopped 140516051592960)>
Joining on thread <Worker(Thread-2, stopped 140516043200256)>
Joining on thread <Worker(Thread-3, stopped 140515961992960)>
Joining on thread <Worker(Thread-4, stopped 140515953600256)>
Joining on thread <Worker(Thread-5, stopped 140515945207552)>
All threads terminated

这引出了一个问题,相对于atexit 处理程序,这个threading._shutdown 例程何时执行? 与atexit 处理程序中的线程交互是否有意义?

【问题讨论】:

标签: python python-3.x locking python-multithreading atexit


【解决方案1】:

您可以使用一个守护线程来要求您的非守护线程优雅地进行清理。举个必要的例子,如果您使用的是启动非守护线程的第三方库,则必须更改该库或执行以下操作:

import threading

def monitor_thread():
    main_thread = threading.main_thread()
    main_thread.join()
    send_signal_to_non_daemon_thread_to_gracefully_shutdown()


monitor = threading.Thread(target=monitor_thread)
monitor.daemon = True
monitor.start()

start_non_daemon_thread()

将它放在原始海报代码的上下文中(注意我们不需要 atexit 函数,因为在所有非守护线程停止之前不会调用它):

if __name__ == "__main__":
    threads = [
        Worker()
        for _ in range(5)
    ]
    
    for t in threads:
        t.start()

    for t in threads:
        t.send_message("Hello")
        #t.send_message(Terminate)

    def monitor_thread():
        main_thread = threading.main_thread()
        main_thread.join()
        shutdown_threads(threads)

    monitor = threading.Thread(target=monitor_thread)
    monitor.daemon = True
    monitor.start()

【讨论】:

  • 这对我来说按预期工作,但我注意到如果monitor_thread 不是守护线程,它也可以工作。我的解释是它已经在等待main_thread.join(),因此会在main_thread 退出时唤醒。文档说“守护线程在关闭时突然停止。”,这让我觉得这里我们实际上可能希望monitor 成为守护线程。
【解决方案2】:

atexit.register(func)func 注册为要在终止时执行的函数。

在主线程中执行最后一行代码(上例中为sys.exit(0))后,(由解释器)调用threading._shutdown等待所有非守护线程(上例中创建的工人)退出

当没有存活的非守护线程时,整个 Python 程序退出。

所以在输入CTRL+C后,主线程被SIGINT信号终止,然后atexit注册的函数被解释器调用。

顺便说一句,如果您将daemon=True 传递给Thread.__init__,程序将直接运行,无需任何人工交互。

【讨论】:

  • 是的,但我希望线程被优雅地杀死,让它们有机会执行清理代码。这就是我的 atexit 处理程序的用途。
  • 如上所述,您可以通过将 Worker 设置为守护线程 (super().__init__(daemon=True) 来实现。
  • 我的理解是守护线程没有机会优雅地处理它们的终止,例如清理他们可能持有的任何资源。它们只是在主线程退出时被残忍地杀死,在运行时关闭过程中根本不考虑。有关使用它们的潜在问题,请参阅此内容,例如:joeshaw.org/python-daemon-threads-considered-harmful
  • atexit 是一个例外,它允许我们在 Python 解释器真正完成之前执行一些清理操作。 > 此时解释器仍然完好无损 (github.com/python/cpython/blob/master/Python/…)
  • 好的,我明白了,这是有道理的。要么我使用守护线程和atexit,要么我必须在退出前“手动”关闭。谢谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-06-07
  • 1970-01-01
  • 2019-04-13
  • 2014-08-18
  • 1970-01-01
相关资源
最近更新 更多