Python 3.7 和 3.8 之间 Python thread.join() 的区别

Posted

技术标签:

【中文标题】Python 3.7 和 3.8 之间 Python thread.join() 的区别【英文标题】:Difference in Python thread.join() between Python 3.7 and 3.8 【发布时间】:2020-11-19 17:52:53 【问题描述】:

我有一个小的 Python 程序,它在 Python 3.7 和 Python 3.8 中的行为不同。我很难理解为什么。 Python 3.8 的 #threading changelog 没有解释这一点。

代码如下:

import time
from threading import Event, Thread


class StoppableWorker(Thread):
    def __init__(self):
        super(StoppableWorker, self).__init__()
        self.daemon = False
        self._stop_event = Event()
    

    def join(self, *args, **kwargs):
        self._stop_event.set()
        print("join called")
        super(StoppableWorker, self).join(*args, **kwargs)

    def run(self):
        while not self._stop_event.is_set():
            time.sleep(1)
            print("hi")

if __name__ == "__main__":
    t = StoppableWorker()
    t.start()
    print("main done.")

当我在 Python 3.7.3 (Debian Buster) 中运行它时,我看到以下输出:

python test.py 
main done.
join called
hi

程序自行退出。我不知道为什么会调用join()。 来自3.7的daemon documentation:

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

但显然线程应该还活着。

当我在 Python 3.8.6 (Arch) 中运行它时,我得到了预期的行为。也就是说,程序一直在运行:

python test.py
main done.
hi
hi
hi
hi
...

3.8 的daemon documentation 声明与 3.7 相同:除非所有非守护线程都已加入,否则程序不应退出。

有人可以帮我理解发生了什么吗?

【问题讨论】:

如果您希望线程优雅地停止,请将它们设为非守护进程并使用合适的信号机制,例如事件 回复,“我不知道为什么会调用 join()。”你能不设置断点,并找出是谁调用的吗?或者,也许更简单,将print("join called") 替换为会引发错误并转储堆栈的语句。 我不完全确定这是对 join 的正确使用,因为有人可能会抱怨 Python 像这样使用或不使用它是错误的。 join 的目的是等待线程结束——而不是向线程发出信号应该结束。这正是 Python 使用它的方式。 【参考方案1】:

线程 _shutdown() 从 Python 版本 3.7.3 到 3.7.4 的行为发生了未记录的变化。

我是这样找到它的:

为了追踪问题,我首先使用inspect 包来找出谁join()是Python 3.7.3 运行时中的线程。我修改了join() 函数以获得一些输出:

...
    def join(self, *args, **kwargs):
        self._stop_event.set()
        c = threading.current_thread()
        print(f"join called from thread c")
        print(f"calling function: inspect.stack()[1][3]")
        super(StoppableWorker, self).join(*args, **kwargs)
...

使用 Python 3.7.3 执行时,会打印:

main done.
join called from thread <_MainThread(MainThread, stopped 139660844881728)>
calling function: _shutdown
hi

因此,已经停止的MainThread 调用join() 方法。 MainThread中负责的函数是_shutdown()

从CPython source for Python 3.7.3 for _shutdown(),第 1279-1282 行:

    t = _pickSomeNonDaemonThread()
    while t:
        t.join()
        t = _pickSomeNonDaemonThread()

MainThread 退出时,该代码在所有非守护线程上调用join()

那个实现是changed in Python 3.7.4。

为了验证这些发现,我从源代码构建了 Python 3.7.4。它的行为确实不同。它使线程按预期运行,并且不会调用join() 函数。

这显然没有记录在release notes of Python 3.7.4 和changelog of Python 3.8 中。

-- 编辑:

正如 MisterMiyagi 在 cmets 中所指出的,有人可能会争辩说,扩展 join() 函数并将其用于发出终止信号并不是正确使用 join()。恕我直言,这取决于口味。但是,应该记录的是,在 Python 3.7.3 及之前的版本中,join() 在系统退出时由 Python 运行时调用,而在更改为 3.7.4 后,情况不再如此。如果记录得当,它将从一开始就解释这种行为。

【讨论】:

Re,“应该……记录一下……join() 是由 Python 运行时调用的。”我不同意。没有理由将其记录在案。对t.join() 的调用不会对线程t 产生任何影响。当您覆盖t.join() 使其确实 影响t 时,这不仅仅是口味问题。你违反了约定:你创建了一个线程对象,它的行为与系统期望线程对象的行为方式不同,然后你把它交给系统并要求系统为你运行它(即,你调用了t.start() 同意@SolomonSlow。每个评论的 3.7.4 更改现在模仿 Thread.join() 而不是直接调用它,可能是由于此类问题...用意外功能覆盖 join()。 查看我对可能解释此 3.7.4 更改的错误修复文档的回答。【参考方案2】:

What's New 仅列出新功能。在我看来,这种变化就像是一个错误修复。 https://docs.python.org/3.7/whatsnew/3.7.html 在顶部附近有一个 changelog 链接。鉴于@Felix 回答中的研究,我们应该查看 3.7.4 中发布的错误修复。 https://docs.python.org/3.7/whatsnew/changelog.html#python-3-7-4-release-candidate-1

这可能是问题所在:https://bugs.python.org/issue36402 bpo-36402:在等待线程时修复 Python 关闭时的竞争条件。等到所有非守护线程的 Python 线程状态被删除(加入所有非守护线程),而不是等到非守护 Python 线程完成。

【讨论】:

以上是关于Python 3.7 和 3.8 之间 Python thread.join() 的区别的主要内容,如果未能解决你的问题,请参考以下文章

我应该如何从 Python 3.8 降级到 Python 3.7? [复制]

使用 python 3.8+(默认协议=5)时,pickle.load 在 python 3.7 中的(协议=4)对象上失败

SICP_3.7-3.8

薛毅书习题 3.5,3.7,3.8,3.10,3.11

3.7 su命令 3.8 sudo命令 3.9 限制root远程登录

3.7 su命令 3.8 sudo命令 3.9 限制root远程登录