为什么线程要持续存在并阻止其进程退出,即使在其目标完成后也是如此?
虽然这个问题使用了额外的子进程,但根本问题完全植根于多线程。因此,这个基本问题可以单独使用MainProcess 重现。可以在edit 2 中找到涉及额外子进程的答案。
场景
如果没有看到您的子进程中的新线程实际上在做什么,您观察到的行为的一个可能场景是您的 thread-1
正在开始又一个 thread-2,你甚至可能不知道。可能它是从您调用的第三方库开始的,或者为了留在 stdlib 中,multiprocessing.Queue.put() 也会在后台启动一个 feeder-thread。
这种一般情况不是Process-subclassing-issue,也与从子进程本身调用Process.close() 无关(使用不正确,但没有后果)。
进程中的MainThread 始终是进程中退出的最后一个线程,它作为_shutdown() 例程的一部分加入非守护进程 线程。这就是让MainThread 在其“表面”工作已经完成的情况下处于不确定状态的原因。
问题在于我在 Task 的 run() 方法中启动的非守护线程。所以我可以肯定地说,即使在它的MainThread 完成之后,线程也会阻止我的进程关闭。但我仍然感到困惑,因为该非守护线程的目标函数已成功完成。
现在在这个图示场景中,thread-1 的目标函数 可以 成功完成。然而,这个thread-1 已经启动了另一个thread-2,然后它会做一些持续很长时间的事情,比如在最坏的情况下永远阻塞。
问:
如果thread-1本身不是问题,为什么把thread-1做成daemon时没有挂起?
这是因为守护进程标志的"initial value is inherited from the creating thread"。因此,使thread-1 成为daemon,也使其后代thread-2 成为daemon,除非明确设置thread-2 的daemon 标志。关闭时不加入守护进程,整个进程“在没有活动的非守护线程时退出”。
请注意,在Python 3.7 之前,Process 创建的非守护线程未加入。
MainProcess 之外的线程的这种不同行为已在 bpo-18966 中得到修复。
代码
为了表明这种情况已经可以通过更简单的设置重现,下面的示例使用MainProcess 作为不会退出的进程。
thread-2 这里是一个Timer-thread,它将在10秒后启动并调用threading.Barrier(parties=1).wait()。然后这个.wait() 调用将立即以parties=1 结束,或者用parties=2 永久阻止,因为在我们的设置中不存在在此Barrier 上调用.wait() 的其他方。这样可以轻松切换我们想要重现的行为。
import threading
def blackbox(parties):
"""Dummy for starting thread we might not know about."""
timer = threading.Timer(10, threading.Barrier(parties=parties).wait) # Thread-2
timer.name = "TimerThread"
timer.start()
def t1_target(parties): # Thread-1
"""Start another thread and exit without joining."""
logger = get_mp_logger()
logger.info(f"ALIVE: {[t.name for t in threading.enumerate()]}")
blackbox(parties)
logger.info(f"ALIVE: {[t.name for t in threading.enumerate()]}")
logger.info("DONE")
if __name__ == '__main__':
import logging
parties = 1
daemon = False
print(f"parties={parties}, daemon={daemon}")
logger = get_mp_logger(logging.INFO)
logger.info(f"ALIVE: {[t.name for t in threading.enumerate()]}")
t = threading.Thread(target=t1_target, args=(parties,), daemon=daemon)
t.start()
t.join()
logger.info(f"ALIVE: {[t.name for t in threading.enumerate()]}")
logger.info("DONE")
下面的日志是针对parties=1的,所以没有无限阻塞,但是由于thread-2不是守护线程,MainThread会在关机时加入它。请注意,在完成t1_target 之后,TimerThread 仍然存在。这里的主要兴趣是MainThread 如何从"DONE" 到"process shutting down" 需要大约10 秒。这是TimerThread 还活着的 10 秒。
parties=1, daemon=False
[18:04:31,977 MainThread <module>] ALIVE: ['MainThread']
[18:04:31,977 Thread-1 t1_target] ALIVE: ['MainThread', 'Thread-1']
[18:04:31,978 Thread-1 t1_target] ALIVE: ['MainThread', 'Thread-1', 'TimerThread']
[18:04:31,978 Thread-1 t1_target] DONE
[18:04:31,978 MainThread <module>] ALIVE: ['MainThread', 'TimerThread']
[18:04:31,978 MainThread <module>] DONE
[18:04:41,978 MainThread info] process shutting down
Process finished with exit code 0
parties=2 在这个阶段永远挂起,...
parties=2, daemon=False
[18:05:06,010 MainThread <module>] ALIVE: ['MainThread']
[18:05:06,010 Thread-1 t1_target] ALIVE: ['MainThread', 'Thread-1']
[18:05:06,011 Thread-1 t1_target] ALIVE: ['MainThread', 'Thread-1', 'TimerThread']
[18:05:06,011 Thread-1 t1_target] DONE
[18:05:06,011 MainThread <module>] ALIVE: ['MainThread', 'TimerThread']
[18:05:06,011 MainThread <module>] DONE
...除非您还设置了daemon=True,否则为thread-1(thread-2 继承)或直接为thread-2。
parties=2, daemon=True
[18:05:35,539 MainThread <module>] ALIVE: ['MainThread']
[18:05:35,539 Thread-1 t1_target] ALIVE: ['MainThread', 'Thread-1']
[18:05:35,539 Thread-1 t1_target] ALIVE: ['MainThread', 'Thread-1', 'TimerThread']
[18:05:35,539 Thread-1 t1_target] DONE
[18:05:35,539 MainThread <module>] ALIVE: ['MainThread', 'TimerThread']
[18:05:35,539 MainThread <module>] DONE
[18:05:35,539 MainThread info] process shutting down
Process finished with exit code 0
助手
DEFAULT_MP_FORMAT = \
'[%(asctime)s,%(msecs)03d %(threadName)s %(funcName)s]' \
' %(message)s'
DEFAULT_DATEFORMAT = "%H:%M:%S" # "%Y-%m-%d %H:%M:%S"
def get_mp_logger(level=None, fmt=DEFAULT_MP_FORMAT, datefmt=DEFAULT_DATEFORMAT):
"""
Initialize multiprocessing-logger if needed and return reference.
"""
import multiprocessing.util as util
import logging
logger = util.get_logger()
if not logger.handlers:
logger = util.log_to_stderr(level)
logger.handlers[0].setFormatter(logging.Formatter(fmt, datefmt))
return logger