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)对象上失败