使用atexit终止线程时,脚本卡在出口上

我正在使用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

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

addsion 回答:使用atexit终止线程时,脚本卡在出口上

您可以使用一个守护程序线程来要求您的非守护程序线程正常清理。对于需要执行此操作的示例,如果使用的是启动非守护线程的第三方库,则必须更改该库或执行以下操作:

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()
,
  

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

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

  

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

因此,在键入 CTRL + C 之后,主线程被SIGINT信号终止,然后解释器调用 atexit 注册函数。

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

本文链接:https://www.f2er.com/3082711.html

大家都在问