为啥我需要显式分离短期变量?

Posted

技术标签:

【中文标题】为啥我需要显式分离短期变量?【英文标题】:Why do I need to explicitly detach a short term variable?为什么我需要显式分离短期变量? 【发布时间】:2018-07-23 14:04:01 【问题描述】:

假设我有一个小操作,我想在一个单独的线程中执行。我不需要知道它何时完成,也不需要等待它完成,但我不希望该操作阻塞我当前的线程。当我编写以下代码时,我会崩溃:

void myFunction() 
    // do other stuff
    std::thread([]()
    
        // do thread stuff
    );

通过将线程分配给变量并将其分离来解决此崩溃:

void myFunction() 
    // do other stuff
    std::thread t([]()
    
        // do thread stuff
    );
    t.detach();

为什么需要这一步?或者有没有更好的方法来创建一个小的一次性线程?

【问题讨论】:

std::thread([]() ).detach(); 怎么样?是缺少变量导致崩溃还是缺少分离? 这看起来不像是重复的。这个问题是问为什么std::thread 有这个设计,而不是如何让崩溃消失。 @Sneftel 这确实是我的问题。 我同意。 OP 已经确定需要.detach - 这是为什么,以及是否有更好的方法。 @GoswinvonBrederlow 这是缺乏分离...所以这是一个答案。 【参考方案1】:

Because the std::thread::~thread() specification says so:

一个线程对象在之后没有关联的线程(并且可以安全地销毁)

它是默认构造的 它被移出 join() 已被调用 detach() 已被调用

看起来detach() 是其中唯一一个对您的情况有意义的,除非您想将线程对象(通过移动)返回给调用者。

为什么需要这一步?

考虑线程对象代表一个长时间运行的执行“线程”(轻量级进程或内核可调度实体或类似实体)。

允许您在线程仍在执行时销毁对象,使您无法随后加入该线程(并找到该线程的结果)。这可能是一个逻辑错误,但它也可能使正确退出程序变得困难。

或者有没有更好的方法来创建一个小的一次性线程?

不是很明显,但通常最好使用线程池在后台运行任务,而不是启动和停止大量短期线程。

您也许可以改用std::async(),但future 在某些情况下,如果您尝试丢弃它,它会在析构函数中返回可能块。

【讨论】:

【参考方案2】:

见the documentation of the destructor of std:thread:

如果*this 有关联的线程(joinable() == true),则调用std::terminate()

您应该明确表示您不关心线程会发生什么,并且您可以对它失去任何控制。这就是detach 的用途。

一般来说,这看起来像是一个设计问题,所以崩溃是有道理的:很难就在这种情况下应该发生什么提出一个普遍且不令人惊讶的规则(例如,您的程序也可以正常结束其执行 - 应该发生什么用线程?)。

【讨论】:

【参考方案3】:

基本上,您的用例需要调用detach(),因为您的用例很奇怪,而不是 C++ 试图简化的。

虽然 Java 和 .Net 可以轻松地让您放弃关联线程仍在运行的 Thread 对象,但在 C++ 模型中,Thread 在某种意义上更接近于成为线程Thread 对象的存在与它所指的执行的生命周期或至少可连接性相吻合。请注意,不启动 Thread 是不可能创建它的(除了默认构造函数,它实际上只是在移动语义的服务中),或者复制它或从线程 id 创建一个。 C++ 希望Thread 比线程寿命更长。

保持这种状态有很多好处。线程控制数据的最终清理不必由操作系统自动完成,因为一旦Thread 消失,就没有任何东西可以尝试加入它。确保具有线程存储的变量及时销毁更容易,因为主线程是最后一个退出的(除非有一些移动恶作剧)。缺少的join - 这是一个非常常见的错误类型 - 在运行时会被正确标记。

相比之下,让一些线程飘到远处是允许的,但这是不寻常的事情。除非它通过同步对象与您的其他线程交互,否则无法确保它完成了它应该做的任何事情。一个分离的线程在reinterpret_cast 的级别上:你可以告诉编译器你知道一些它不知道的东西,但这必须是明确的,而不仅仅是你不知道的函数的结果 打电话。

【讨论】:

IMO 想要创建一个线程然后忘记它并没有什么奇怪的。 Java 世界为这种模式取了一个名字:守护线程 位于后台,只要进程继续运行,等待并处理一些重复发生的事件。 (相关:en.wikipedia.org/wiki/Crash-only_software)。 @jameslarge Java 具有不同的线程终止语义,这使得它成为一种更实用的模式。在 C++ 中运行这样的线程是非常危险的。 线程终止语义? Java 运行时环境不会在 Java 进程终止之前向现有线程发出任何警告。 (可以注册一个将在 new 线程中调用的处理函数,以通知应用程序 clean JVM 关闭。)但是,我不明白 Java 如何守护线程隐式地比 C++ 分离线程的危险性低。 @jameslarge 因为 Java 线程可以在运行时需要时离开。由于 Java 是经过 GC 处理的,因此无需展开堆栈,这在 RAII-ish 语言中通常不可能异步执行。请注意,Java 的Thread.stop() 缺少任何 C++ 对应项。 (很明显,Java 中的用户代码也不是你想要的东西,但底层功能是 Java 中守护线程如何工作的核心。) @jameslarge 此外,JVM 从运行时被沙盒化。这意味着在任何给定时刻(或至少在任何安全点),Java 线程都可以在不破坏内存分配器的情况下被终止。 C++ 就不行了,这使得在任何其他线程正在运行时稳健地关闭运行时是不可行的。【参考方案4】:

考虑一下:线程 A 创建线程 B 并且线程 A 离开其执行范围。线程 B 的句柄即将丢失。现在应该怎么办?有几种可能性,最明显的如下:

    线程 B 已分离并继续独立执行 线程 A 在退出自己的作用域之前等待(加入)线程 B

现在您可以争论哪个更好:1 还是 2?我们(编译器)应该如何决定其中哪一个更好?

所以设计者所做的是不同的:crash 终止代码,以便开发人员明确地选择这些解决方案之一。为了避免隐含的(也许是不需要的)行为。这是给你的一个信号:“嘿,现在注意,这段代码很重要,我(编译器)不想为你做决定”。

【讨论】:

任何一种方式都可以接受。但我发现令人不安的是“我会崩溃”的行为。析构函数应该做一些理智的事情。例如默认是等待。除非你分开。线程内部可以有一个标志,因此析构函数知道它何时被分离。好吧,它必须已经有了,否则分离不会阻止崩溃。注意:python 等待。并不总是最好的,但比崩溃更好(我假设是段错误)。 @GoswinvonBrederlow 我知道两者都可以接受。但是引入了“我会崩溃”的行为,以便编码人员做出明确的决定。默认情况下加入会引入开销。默认情况下分离很容易出错。选择哪一个作为默认行为并不是那么明显。这就是我想象 C++ 开发人员的思维方式。 顺便说一句,这不是段错误。很简单的终止。 啊,terminate() 是另外一回事。抱歉,我把“崩溃”理解为字面意思。

以上是关于为啥我需要显式分离短期变量?的主要内容,如果未能解决你的问题,请参考以下文章

`.vue` 组件分离/就绪函数中的显式调用 vuex 操作

如何分离SQL数据库,为啥我一分离就没有了

为啥 EF 在分离时会删除子实体?

为啥我不需要显式借出一个借来的可变变量?

为啥需要显式定义静态变量?

为啥我不能从 FragmentPagerAdapter 分离片段?