为啥必须在线程销毁之前调用 join() 或 detach() ?

Posted

技术标签:

【中文标题】为啥必须在线程销毁之前调用 join() 或 detach() ?【英文标题】:Why must one call join() or detach() before thread destruction?为什么必须在线程销毁之前调用 join() 或 detach() ? 【发布时间】:2019-11-25 18:08:48 【问题描述】:

我不明白为什么当 std::thread 被破坏时,它必须处于 join() 或 detach() 状态。

Join 等待线程完成,而 detach 不会。 似乎有一些我不理解的中间状态。 因为我的理解是 join 和 detach 是互补的:如果我不调用 join() 而不是 detach() 是默认的。

这样说吧,假设您正在编写一个创建线程的程序,并且仅在该线程生命周期的后期才调用 join(),所以在调用 join 之前,线程基本上都在运行,就好像它是分离,不是吗?

逻辑上 detach() 应该是线程的默认行为,因为这是线程的定义,它们并行执行而与其他线程无关。

那么当线程对象被破坏时,为什么要调用 terminate() 呢?为什么标准不能简单地将线程视为分离?

我不理解在线程被破坏之前没有调用 join() 或 detached() 时终止程序的基本原理。这样做的目的是什么?

更新:

我最近遇到了这个。 Anthony Williams 在他的《Concurrency In Action》一书中指出,“C++17 的一个提议是关于一个与 std::thread 类似的 join_thread 类,除了它会像 scoped_thread 一样自动加入析构函数。这没有在委员会中达成共识,因此没有被标准接受(尽管它仍然在 C++20 中作为 std::jthread 的轨道)......”

【问题讨论】:

有一个关于这个的讨论,我将尝试找到链接,因为这种行为与boost::thread 的行为不同。最终,尽管他们决定调用std::terminate,如果它是joinable 另见:When should I use std::thread::detach ?:在其他线程运行时让main 线程退出存在问题。 自动加入析构函数只是交易的一部分。例如,如果您将一个线程移动到一个可连接线程,它也会终止。 【参考方案1】:

问题:“那么当线程对象被破坏时,为什么要调用terminate()?为什么标准不能简单地将线程视为已分离?”

回答:是的,我同意它严重终止程序,但这样的设计有其原因。如果在析构函数std::thread::~thread 中没有std::terminate() 机制,如果用户真的想做join(),但由于某种原因“join”没有执行(例如抛出异常),那么 new_thread 将在后台运行就像detach() 行为一样。这可能会导致未定义的行为,因为这不是用户拥有分离线程的初衷。

【讨论】:

【参考方案2】:

当活动线程的线程句柄超出范围时,您有两种选择:

    加入 分离 杀死线程 杀死程序

这些选项中的每一个都很糟糕。无论您选择哪一个,它都会令人惊讶、令人困惑,而且在大多数情况下都不是您想要的。


可以说,您提到的连接线程已经以 std::async 的形式存在,它为您提供了一个 std::future,它会阻塞直到创建的线程完成,因此进行隐式连接。但是关于为什么的许多问题

std::async(std::launch::async, f);
g();

没有同时运行fg 表明这是多么令人困惑。我知道的最佳方法是将其定义为编程错误并让程序员修复它,因此assert 是最合适的。不幸的是,该标准改为使用std::terminate


如果您真的想要一个分离线程,只需在 std::thread 周围编写一个小包装器,在其析构函数或您想要的任何处理程序中执行 if (thread.joinable()) thread.detach();

【讨论】:

【参考方案3】:

您可能希望线程在完成后完全清理,不留下任何痕迹。这意味着您可以启动一个线程然后忘记它。

但您可能还希望能够在线程运行时对其进行管理,并获得它在完成时提供的任何返回值。在这种情况下,如果一个线程在完成后自行清理,您尝试管理它可能会导致崩溃,因为您将访问可能无效的句柄。并且要在线程结束时检查返回值,必须将返回值存储在某个地方,这意味着无法完全清理线程,因为必须保留存储返回值的位置。

在大多数框架中,默认情况下,您会获得第二个选项。您可以管理线程(通过中断它、向它发送信号、加入它或其他方式),但它无法自行清理。如果您更喜欢第一个选项,则有一个函数可以获取该行为(分离),但这意味着您可能无法访问该线程,因为它可能会或可能不会继续存在。

【讨论】:

【参考方案4】:

从技术上讲,答案是“因为规范是这样说的”,但这是一个迟钝的答案。我们无法读懂设计者的想法,但这里有一些问题可能有所贡献:

对于 POSIX pthread,子线程必须在它们退出后才加入,否则它们会继续占用系统资源(如内核中的进程表条目)。这是通过pthread_join() 完成的。 如果进程持有子线程的 HANDLE,Windows 就会出现类似的问题。尽管 Windows 不需要完全连接,但该进程仍必须调用 CloseHandle() 以释放其在线程上的引用计数。

由于std::thread 是一个跨平台的抽象,它受到需要连接的POSIX 要求的约束。

理论上std::thread 析构函数可以调用pthread_join() 而不是抛出异常,但是(主观上)这可能会增加死锁的风险。而正确编写的程序会知道何时在安全的时间插入连接。

另见:

https://en.wikipedia.org/wiki/Zombie_process https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessa https://docs.microsoft.com/en-us/windows/win32/procthread/terminating-a-process

【讨论】:

这里的 POSIX 和 Windows 之间没有真正的区别——正在发生的事情是进程持有对线程的引用,直到调用(并完成)加入或分离,这意味着线程资源在 refcount 降至 0 之前无法清理。【参考方案5】:

您会感到困惑,因为您将std::thread 对象与其所指的执行线程混为一谈。 std::thread 对象是一个 C++ 对象(内存中的一堆字节),它充当对执行线程的引用。当您调用std::thread::detach 时,会发生std::thread 对象与执行线程“分离”——它不再指代(任何)执行线程,并且执行线程继续独立运行。但是std::thread 对象仍然存在,直到它被销毁。

当一个线程执行完成时,它将其退出信息存储到引用它的std::thread 对象中,如果有一个(如果它已分离,则没有一个,所以退出信息只是抛出它对std::thread 对象没有其他影响——尤其是std::thread 对象不会被破坏,并且会继续存在,直到其他人破坏它。

【讨论】:

我知道std::thread 只是一个处理程序,并且您似乎是在说,如果未调用 detach 来销毁处理程序,那么实际线程可能会被绑定到一个无效的处理程序/对象。我知道这是会发生的事情,但为什么不默认调用 detach(),对我来说,在销毁之前调用 detach 更有意义,这本质上是诉诸于被认为是默认行为的行为。在实施这种似乎有点违反直觉的行为时,标准考虑了什么? @MosheRabaev 迫使你思考并决定应该如何处理线程的结尾。 @MosheRabaev:如果你想要这种行为,你可以在th被销毁之前粘贴if (th.joinable()) th.detach();(你甚至可以将std::thread包装在你自己的线程对象中,它在析构函数中执行此操作。 ) 我理解 OrangeDog 所说的和 fifoforlifo 给出的答案,它让你思考你在做什么以避免潜在的问题,所以我会坚持给出的:)

以上是关于为啥必须在线程销毁之前调用 join() 或 detach() ?的主要内容,如果未能解决你的问题,请参考以下文章

C++11多线程,线程对象(thread对象)joinable()join()detach()左值智能指针

调用 join() 之前取消线程会报错

为啥 Python 在对象之前销毁类变量?

多线程-join()方法

Linux 进程与线程二

java join 方法的使用