我什么时候应该使用std::thread::detach?

203
有时我需要使用std::thread来加快我的应用程序。我也知道join()会等待线程完成。这很容易理解,但是调用detach()和不调用它有什么区别呢?
我认为如果没有detach(),线程的方法将独立地使用一个线程。
不分离:
void Someclass::Somefunction() {
    //...

    std::thread t([ ] {
        printf("thread called without detach");
    });

    //some code here
}

使用分离调用:

void Someclass::Somefunction() {
    //...

    std::thread t([ ] {
        printf("thread called with detach");
    });

    t.detach();

    //some code here
}

1
可能是Detached vs. Joinable POSIX threads的重复问题。 - n. m.
1
stdboost 线程都有 detachjoin,这两个函数的模型是基于 POSIX 线程的。 - n. m.
6个回答

226

std::thread的析构函数中,如果:

  • 线程没有被加入(使用t.join()
  • 也没有被分离(使用t.detach()

那么会调用std::terminate

因此,在执行流到达析构函数之前,您应该始终joindetach线程。


当程序终止(即main返回)时,仍在后台执行的剩余分离线程不会被等待;相反,它们的执行被暂停,并且它们的线程本地对象不会被销毁。

关键是,这意味着这些线程的堆栈不会被展开,因此某些析构函数不会被执行。根据这些析构函数应该执行的操作,这可能是一种糟糕的情况,就像程序崩溃或被杀死一样。希望操作系统会释放文件锁等...但您可能会损坏共享内存、半写入的文件等。


那么,你应该使用 join 还是 detach

  • 使用 join
  • 除非你需要更多的灵活性并且愿意提供同步机制来等待线程完成,自己处理,否则你可以使用 detach

如果我在main()函数中调用pthread_exit(NULL);,那么exit()将不会从main()函数中被调用,因此程序将继续执行,直到所有已分离的线程完成。然后才会调用exit()。 - Kurovsky
2
@Matthieu,为什么我们不能在std :: thread的析构函数中加入join()? - john smith
2
@johnsmith:一个非常好的问题!当你加入时会发生什么?你需要等待线程完成。如果抛出异常,析构函数将被执行... 突然间,你的异常传播被暂停,直到线程终止。有很多原因导致它不会终止,特别是如果它正在等待当前挂起的线程输入!因此,设计者选择使其成为明确的选择,而不是选择有争议的默认值。 - Matthieu M.
@Matthieu 我想你的意思是在达到std::thread的析构函数之前调用join()。您可以(而且应该?)加入一个封闭类的析构函数吗? - Jose Quinteiro
6
与其他资源不同,建议不要在析构函数中加入join。问题在于,加入操作并不会结束线程,而仅仅是等待它结束;如果没有一个信号来终止该线程,你可能需要等待很长时间,从而会阻塞当前正在被卸载的线程栈,并且防止this current thread终止,因此也会阻塞等待其的线程等等。因此,除非您确信能够在合理的时间内停止给定的线程,否则最好不要在析构函数中等待它结束。 - Matthieu M.
显示剩余4条评论

49

如果您不打算使用join等待线程完成,而是让该线程一直运行直到完成并在没有生成线程特定的等待情况下终止,那么您应该调用detach

std::thread(func).detach(); // It's done when it's done

detach基本上会释放实现join所需的资源。

如果一个线程对象结束其生命周期,既没有调用join,也没有调用detach,则会发生致命错误,在这种情况下会调用terminate


40
该回答旨在回答标题中的问题,而不是解释join和detach之间的区别。那么什么时候应该使用std :: thread :: detach?
在正确维护的C ++代码中,根本不应使用std :: thread :: detach。程序员必须确保所有创建的线程都优雅地退出,释放所有已获取的资源并执行其他必要的清理操作。这意味着通过调用detach放弃线程所有权不是一个选项,因此在所有场景中应该使用join。
然而,一些应用程序依赖于旧的、经常不太好设计和支持的API,这些API可能包含无限期阻塞的函数。将这些函数的调用移动到一个专用线程中以避免阻塞其他内容是一种常见做法。没有办法使这样的线程优雅地退出,因此使用join只会导致主线程阻塞。这就是使用detach的情况,它比分配具有动态存储期的线程对象,然后故意泄漏它要少得多的替代方案。
#include <LegacyApi.hpp>
#include <thread>

auto LegacyApiThreadEntry(void)
{
    auto result{NastyBlockingFunction()};
    // do something...
}

int main()
{
    ::std::thread legacy_api_thread{&LegacyApiThreadEntry};
    // do something...
    legacy_api_thread.detach();
    return 0;
}

我不会说这仅适用于遗留API,例如std::getline将无限期阻塞。 - Hugo Burd
1
"std::thread::detach 不应该被使用" 这简直不是真的。如果你有一个线程应该一直存在到程序结束,你可以将其分离,并且事实上,如果你无法保证线程的完成,比如线程包含一个信号处理程序并且有时会调用 exit 而不是返回,那么你应该将其分离。等待永远不返回的线程会锁住你的程序。 - Reverent Lapwing
@ReverentLapwing 我认为你应该读完我回答的整个句子。我提到了一个可能有效的分离线程的原因。我还应该补充说明:1)当线程包含信号处理程序时(如果我们谈论的是posix信号),优雅退出没有问题;2)在应用程序中任何地方调用exit甚至比调用detach更糟糕,因为不会调用任何析构函数或进行任何清理。 - user7860670
1
@starriet 系统在进程退出时会清理进程使用的内存和已打开的文件句柄。然而,不执行任何用户指定的清理操作。基本上,分离的线程会在它们正在进行的任何操作中间被终止。例如,如果分离的线程一直在写入某个缓冲文件流,则该流不会被刷新,累积的数据将会丢失。 - user7860670
1
@starriet 这里使用 detach 是因为调用 join 会导致进程无限期地挂起,直到 NastyBlockingFunction 返回,而将线程保留在可连接状态会导致线程析构函数调用 std::terminate 从而崩溃进程。 - user7860670
显示剩余8条评论

17

当你分离线程时,意味着在退出 main() 前不必使用 join()

线程库会等待每个 低于-main 的线程,但这对你来说无关紧要。

detach() 主要在你需要在后台完成任务但不关心执行过程的情况下使用。通常一些库会静默地创建一个后台工作线程并将其分离,这样你甚至都不会注意到它的存在。


2
这并没有回答问题。答案基本上是说“你在分离时分离”。 - rubenvb

2
根据 cppreference.com 的说明:

将线程的执行与线程对象分离,使其能够独立继续执行。一旦线程退出,任何已分配的资源都将被释放。

调用 detach 后,*this 不再拥有任何线程。

例如:
  std::thread my_thread([&](){XXXX});
  my_thread.detach();

注意本地变量:my_thread,当my_thread的生命周期结束时,将调用std::thread的析构函数,并且在析构函数中调用std::terminate()
但是如果使用detach(),即使my_thread的生命周期已经结束,也不应再使用my_thread,新线程不会受到影响。

好的,我收回刚才说的话。@TobySpeight - DinoStray
8
请注意,如果在lambda捕获中使用&,则您正在使用封闭作用域的变量引用 - 因此,您最好确保您引用的任何变量的生命周期比线程生命周期更长。 - DavidJ
1
@DavidJ 使用 [=] 可能会解决那个问题。 - user1300214

1
也许重申一下上面某个答案中提到的是个好主意:当主函数完成并且主线程正在关闭时,所有生成的线程都将被终止或挂起。因此,如果您依赖于分离来使后台线程在主线程关闭后继续运行,那么您会感到惊讶。要查看效果,请尝试以下操作。如果取消最后一个睡眠调用的注释,则输出文件将被创建并写入得很好。否则不会:
#include <mutex>
#include <thread>
#include <iostream>
#include <fstream>
#include <array>
#include <chrono>

using Ms = std::chrono::milliseconds;

std::once_flag oflag;
std::mutex mx;
std::mutex printMx;
int globalCount{};
std::ofstream *logfile;
void do_one_time_task() {
    //printMx.lock();
    //std::cout<<"I am in thread with thread id: "<< std::this_thread::get_id() << std::endl;
    //printMx.unlock();
    std::call_once(oflag, [&]() {
    //  std::cout << "Called once by thread: " << std::this_thread::get_id() << std::endl; 
    //  std::cout<<"Initialized globalCount to 3\n";
        globalCount = 3;
        logfile = new std::ofstream("testlog.txt");
        //logfile.open("testlog.txt");
        });
    std::this_thread::sleep_for(Ms(100));
    // some more here
    for(int i=0; i<10; ++i){    
        mx.lock();
        ++globalCount;
        *logfile << "thread: "<< std::this_thread::get_id() <<", globalCount = " << globalCount << std::endl;
        std::this_thread::sleep_for(Ms(50));
        mx.unlock();
        std::this_thread::sleep_for(Ms(2));
    }

    std::this_thread::sleep_for(Ms(2000));
    std::call_once(oflag, [&]() {
        //std::cout << "Called once by thread: " << std::this_thread::get_id() << std::endl;
        //std::cout << "closing logfile:\n";
        logfile->close();
        });

}

int main()
{
    std::array<std::thread, 5> thArray;
    for (int i = 0; i < 5; ++i)
        thArray[i] = std::thread(do_one_time_task);

    for (int i = 0; i < 5; ++i)
        thArray[i].detach();

    //std::this_thread::sleep_for(Ms(5000));
    std::cout << "Main: globalCount = " << globalCount << std::endl;

    return 0;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接