【问题标题】:Process with finished thread never exits线程完成的进程永远不会退出
【发布时间】:2020-05-04 21:07:00
【问题描述】:

为什么线程要持续存在并阻止其进程退出,即使它的目标已经完成?

虽然这个问题使用了额外的子进程,但根本问题完全植根于多线程。因此,这个基本问题可以单独使用MainProcess 重现。 (由@Darkonaut 编辑)

我创建了一个继承multiprocessing.Process的类:

class Task(Process):
    def run(self) :
        print("RUN")

        t = threading.Thread(target=do_some_work)
        t.start()
        # ...
        t.join()
        print("CLOSED")

我是这样开始的:

proc = Task()
proc.start()
proc.join()
print("JOINED")

但它不会加入,输出会是这样的:

>> RUN
>> CLOSED

我没有使用任何类型的QueuesPipes

当我在 Ubuntu 上运行它时,我用它的 pid 跟踪了这个进程。即使在print("CLOSED") 行完成后,该进程仍然存在,没有任何例外。我还在 Windows 上运行了它,并在任务管理器中跟踪了这个过程。进程在print("CLOSED") 之后退出,仍然没有加入。

另一点是,在 Ubuntu 上,当一切都卡在 print("CLOSED") 之后并且我按下 Ctrl + C 时,我得到这个:

Traceback (most recent call last):
  File "Scheduler.py", line 164, in <module>
    scheduler.start()
  File "Scheduler.py", line 152, in start
    self.enqueueTask(plan)
  File "Scheduler.py", line 134, in enqueueTask
    proc.join()
  File "/usr/local/lib/python3.8/multiprocessing/process.py", line 149, in join
    res = self._popen.wait(timeout)
  File "/usr/local/lib/python3.8/multiprocessing/popen_fork.py", line 47, in wait
    return self.poll(os.WNOHANG if timeout == 0.0 else 0)
  File "/usr/local/lib/python3.8/multiprocessing/popen_fork.py", line 27, in poll
    pid, sts = os.waitpid(self.pid, flag)

根据最后一行,我猜主进程正在等待什么,但是什么以及为什么?

问题似乎与我在Taskrun() 方法中启动的非守护线程有关。 使这个线程成为守护线程解决了这个问题,所以我可以肯定地说这个线程正在阻止我的进程即使在它的MainThread 完成之后也被关闭。我仍然很困惑,因为那个非守护线程的目标函数已经成功完成了。

【问题讨论】:

  • 文档说close()成功返回后,大多数其他方法都不起作用。在调用 join() 之前尝试检查 is_alive()
  • 基本上父进程正在等待SIGCHLD信号。见:geeksforgeeks.org/exit-status-child-process-linux
  • 我无法在 Ubuntu、Python 3.7.3 上复制它。当您只运行您向我们展示的代码时,问题是否仍然存在?无论如何,你不应该打电话给Process.close() a)当孩子还在跑步时,b)从孩子本身。
  • @Darkonaut 请阅读更新 1,我想知道为什么非守护线程应该存在并防止进程被杀死,即使它的目标在 30 秒后完成?
  • self.close() 现在被删除了吗?我们需要查看一个最小的线程示例,重现问题。

标签: python multithreading multiprocessing python-multiprocessing python-multithreading


【解决方案1】:

为什么线程要持续存在并阻止其进程退出,即使在其目标完成后也是如此?

虽然这个问题使用了额外的子进程,但根本问题完全植根于多线程。因此,这个基本问题可以单独使用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-2daemon 标志。关闭时不加入守护进程,整个进程“在没有活动的非守护线程时退出”。

请注意,在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-1thread-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

【讨论】:

  • 这样一个惊人的答案!
  • @AmirHmZ 但是,它是否符合您的要求?一个神秘的第二个线程是我能想到的唯一重现您观察到的行为的场景。
  • @AmirHmZ 在 Python 中,您可以使用 threading.enumerate() 跟踪进程中正在运行的线程。恐怕没有真正令人满意的答案。我正在使用我的示例中的日志记录或打印来找到它开始挂起的点。如果必要和适用,也可以在标准库代码中。然后,已经了解或阅读了有关通常会导致挂起的原因的文档,可以让您进行调查。
  • 我使用 threading.enumerate() 跟踪了所有线程,我发现我的套接字侦听器线程一直在运行:|所以将其更改为守护线程,问题就完全解决了。非常感谢:)
  • @AmirHmZ 不客气! :) 我很高兴能帮上忙,当然感谢意外的赏金!稍后我可能会编辑您的问题,以便更多可能从答案中受益的人有机会找到它。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-01-21
  • 2013-11-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多