【问题标题】:Why program is terminated if neither calls std::thread::detach nor std::thread::join?如果既不调用 std::thread::detach 也不调用 std::thread::join,为什么程序会终止?
【发布时间】:2021-04-02 15:16:54
【问题描述】:

我知道如果既没有 std::thread::detach 也没有 std::thread::join 被调用。线程对象在析构函数中调用std::terminate。我想知道这个设计选择了什么?为什么它在析构函数中调用std::terminaten。此外,如果它没有在析构函数中调用std::terminate,则 no-detach 的行为与 detach 现在的行为相同。那么为什么在设计线程API的时候不只保留thread::join而去掉terminate的调用呢?引擎盖下的考虑是什么?

【问题讨论】:

    标签: c++ stdthread


    【解决方案1】:

    这是 C++11 之前很多争论的主题。

    你的问题做了一个大胆的假设:分离显然是正确的行为。但你从来没有证实过它。事实上,有很多反对这个想法的论据,委员会也考虑过。

    我将从outlining the argument against it论文中举这个例子:

    int fib(int n) {
        if (n <= 1) return n;
        int fib1, fib2;
        
        std::thread t([=, &fib1]{fib1 = fib(n-1);});
        fib2 = fib(n-2);
        if (fib2 < 0) throw ...
        t.join();
        return fib1 + fib2;
    }
    

    一旦你开始抛出异常,默认的分离行为就不再那么有用了。实际上,您可以想象一个更复杂的情况,其中异常来自线程创建例程的非本地内容。考虑this example from a later paper

          std::vector<std::pair<unsigned int, unsigned int>> partitions =
            utils::partition_indexes(0, size-1, num_threads);
          std::vector<std::thread> threads;
    
          LOG(LOG_DEBUG, "controller::reload_all: starting reload threads...");
          for (unsigned int i=0; i<num_threads-1; i++) {
            threads.push_back(std::thread(reloadrangethread(this,
            partitions[i].first, partitions[i].second, size, unattended)));
          }
    
          LOG(LOG_DEBUG, "controller::reload_all: starting my own reload...");
          this->reload_range(partitions[num_threads-1].first,
            partitions[num_threads-1].second, size, unattended);
    
          LOG(LOG_DEBUG, "controller::reload_all: joining other threads...");
          for (size_t i=0; i<threads.size(); i++) {
            threads[i].join();
          }   
    

    push_back 可能由于缺少用于重新分配数组的内存而失败。如果发生这种情况,您将无法访问所有这些线程,并且您的程序将被破坏。

    这两种情况都会导致程序损坏,无论您是默认分离还是默认为terminate。但是,如果程序要被破坏,最好在问题发生时立即将其破坏,而不是在代码中稍后的某个位置。

    现在,更安全的解决方案是在析构函数中使用join。但由于其他各种原因,这并没有发生。 (不幸的)共识是,如果你没有说出你想做什么,那么你的代码就会被破坏并且应该爆炸。

    幸运的是,C++20 给了我们std::jthread,它默认加入了它的析构函数。

    std::thread的析构行为的大致思路很简单:

    1. 用户没有说要做什么(即:没有打电话给joindetach)。
    2. 显然没有一个答案是正确的。

    您声称简单地执行detach 是正确的解决方案。但为什么是对的?分离是一件非常不安全的事情,因为您再次无法使用线程join

    还有 RAII 的问题。异常可能会导致某些thread 对象无意中被破坏。如果发生这种情况,并且默认行为是分离,那么您的程序是否仍处于功能状态?如果您的程序的其余部分期望 join 那些线程,而现在这是不可能的呢?

    【讨论】:

      【解决方案2】:

      丢线程是不好的;在几乎所有平台上,你的程序关闭都会变得非常崩溃(我的意思是你会很幸运......)。

      通过“在线程退出时做好准备”期货不会丢失分离​​的线程。但是调用detach() 而不安排在main() 结束之前同步你的线程结束意味着你的程序的行为可能变得未定义C++ 标准(大多数线程中的大多数代码都充满了无法安全的代码在 main 结束后运行,因此在main() 结束后存活的线程不是一个好主意;没有连接或等效项,线程完成和main() 之间存在“竞争”,无论有多少“睡眠” " 调用你。这种竞争的存在通常足以使 C++ 正式注销指定你的程序的行为)。

      “我应该分离”的想法是错误的。线程应该默认为 detach() 的想法很疯狂。

      默认为join()detach() 更合理。但是加入可以抛出。抛出析构函数是不好的,因为它们在抛出过程中被评估,如果在抛出过程中涉及展开的代码依次抛出,则程序终止。更重要的是,如果未经检查,该异常路径很可能包含死锁条件,因为您可能正在与另一个线程握手,并且它不知道要自行关闭。

      std::thread 不是用户友好的安全线程原语;使线程对用户友好远远超出了它的范围。用户友好的安全线程原语可以建立在它之上。这是从原始 pthread 中删除的一步。例如,像 IPP 之类的库。它的作用是使在 C++ 中编写线程代码成为可能,而无需特定于平台的扩展。

      通过在销毁时终止,我们在设计阶段向程序员提供反馈,告诉他们必须明智地处理问题,而不是忽略它们。这使得它使用起来有点困难,但是正确使用线程的 99.9% 的困难不是调用join()

      线程很难正确。当您弄错时,它通常也可以正常工作;然后它很少锁定或崩溃,并且仅在其他一些用户的系统上被剥离符号。一旦您将线程添加到程序中,您就不能依赖“我尝试过并且它有效”。您甚至不能通常依赖“此代码在本地是正确的”,因为大多数并发设计不组合 - 三个成对的“正确”子程序在组合时可能会变得不正确。

      像 TBB 之类的库,或者你自己的库,可以稍微减少这个问题。不可变状态和功能操作也是如此。或者一堆其他严谨的设计。所有这些最终都涉及在 std::thread 这样低级别的基础上编写框架。

      【讨论】:

        【解决方案3】:

        这是一个半答案,只关注“为什么不分离”。

        std::thread 代表一个执行线程。按照 RAII 范式,构造函数创建执行线程,而析构函数销毁它(对当前不代表执行线程的“空”对象进行适当的警告)。从线程中分离并不会结束执行线程,因此这不是析构函数的一个很好的概念。

        加入线程等待执行线程结束,而终止则强制执行线程结束。这些方法中的任何一种都与std::thread 的析构函数在概念上相匹配。我将留给其他人讨论为什么选择一个选项而不是另一个选项。 (简短的版本是有很多争论;毫无疑问,没有一个选项比另一个更好,但必须做出决定。)

        其他人给出了更好的答案。我为那些寻求简短和概念性的人提供这个答案。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多