当main()函数退出时,一个已分离的线程会发生什么?

195
假设我启动了一个 std::thread 并将其 detach(),因此即使原先表示它的 std::thread 超出作用域,该线程仍然继续执行。
进一步假设程序没有可靠的协议来加入已分离的线程1,因此当 main() 退出时,已分离的线程仍在运行。
我在标准(更确切地说是 N3797 C++14 草案)中找不到任何描述应发生什么的内容,既不是 1.10 也不是 30.3 包含有关的措辞。 1 另一个可能等效的问题是:“已分离的线程是否可以再次连接”,因为无论您发明什么协议来连接,信号传递部分都必须在线程仍在运行时完成,并且操作系统调度程序可能会决定将线程休眠一小时,就在信号已发出之后,接收端无法可靠检测到线程实际上已经完成。
如果在 main() 运行完毕时,正在运行的已分离线程是未定义行为,则除非主线程从未退出2,否则任何使用 std::thread::detach() 都是未定义行为。
因此,在已分离的线程运行时退出 main() 必须具有已定义的效果。问题是:这些效果在哪里得到定义(在 C++ 标准中,而不是 POSIX、操作系统文档等)?

2 一个分离的线程无法加入(即不能使用std::thread::join())。您可以等待来自已分离的线程的结果(例如通过std::packaged_task的未来,或通过计数信号量或标志和条件变量),但这并不能保证线程已经执行完成。实际上,除非将信号部分放入线程的第一个自动对象的析构函数中,否则通常会有代码(析构函数)在发出信号后运行。如果操作系统在分离线程完成运行析构函数之前调度主线程以消耗结果并退出,那么会发生什么^W被定义为什么?


5
在[basic.start.term]/4中,我只能找到一条非常模糊的非强制性注释:"在调用std::exit或从main退出之前终止每个线程是足够的,但不是必需的,以满足这些要求。"(可能整段都相关)。另请参阅[support.start.term]/8(当main返回时调用std::exit - dyp
7个回答

57
原始问题“当main()退出时,一个脱离的线程会发生什么” 的答案是:
它会继续运行(因为标准没有说它会停止),只要它不触及其他线程的(自动|线程局部)变量或静态对象,这是明确定义的。
这似乎被允许是为了允许将线程管理器作为静态对象(注意在 [basic.start.term] / 4 中已经说明了,感谢@dyp提供的指针)。
问题出现在静态对象的销毁完成后,因为此时执行进入了只有信号处理程序中允许的代码的范畴([basic.start.term]/1,第一句话)。 根据C++标准库,唯一允许执行的代码是<atomic>库([support.runtime]/9,第二句话)。特别地,通常情况下排除使用condition_variable(因为它不是<atomic>的一部分,因此其是否可在信号处理程序中使用是由实现定义的)。
除非在此时已经解开了栈,否则很难避免未定义的行为。
第二个问题“脱离的线程是否可以重新连接” 的答案是:
可以,使用*_at_thread_exit函数族(notify_all_at_thread_exit()std::promise::set_value_at_thread_exit(),...)。
如问题的脚注[2]所述,仅通过发信号量、条件变量或原子计数器是不足以加入一个已脱离的线程的(即无法确保在等待线程接收到该信号之前其执行已经完成),因为通常情况下,在例如调用notify_all()后仍会执行更多代码,特别是自动和线程局部对象的析构函数。

将信号作为线程最后执行的操作 (自动对象和线程本地对象的析构函数已经发生后) 是 _at_thread_exit 函数族的设计目的。

因此,为了避免在没有任何实现保证的情况下出现未定义的行为,您需要使用一个 _at_thread_exit 函数手动加入一个分离线程并进行信号传递,或者 使分离线程执行对于信号处理程序也安全的代码。


27
你确定这个吗?在我测试的所有环境中(GCC 5,clang 3.5,MSVC 14),当主线程退出时所有分离线程都会被终止。 - rustyx
7
我认为问题不在于特定实现的作用,而在于如何避免标准定义的未定义行为。 - Jon Spencer
15
这个回答似乎暗示着在静态变量被销毁后,进程会进入某种等待所有剩余线程完成的休眠状态。这是不正确的,exit 完成静态对象的销毁、运行 atexit 处理程序、刷新流等操作后,它将控制权返回给主机环境,即进程退出。如果一个分离的线程仍在运行(并且通过不触及其它线程之外的任何东西来避免未定义行为),则随着进程的退出,它就会消失在一团烟中。 - Jonathan Wakely
3
如果你可以使用非 ISO C++ API,那么如果 main 函数调用 pthread_exit 而不是返回或调用 exit,那么这将导致进程等待分离的线程完成,并在最后一个线程完成后调用 exit - Jonathan Wakely
5
它继续运行(因为标准没有说明它停止了)--> 有人能告诉我一个线程如何在没有其容器进程的情况下继续执行吗? - TonySalimi
显示剩余6条评论

49

分离线程

根据std::thread::detach:

将执行线程与线程对象分离,允许执行独立进行。一旦线程退出,任何已分配的资源都将被释放。

根据pthread_detach

pthread_detach()函数应指示实现在线程终止时可以回收线程的存储空间。如果线程尚未终止,则pthread_detach()不会导致其终止。对同一目标线程进行多个pthread_detach()调用的影响是未指定的。

分离线程主要是为了节省资源,以防应用程序不需要等待线程完成(例如必须运行直到进程终止的守护进程):

  1. 为释放应用程序端的句柄:可以让一个 std::thread 对象超出作用域而不加入,这通常会在销毁时调用 std::terminate()
  2. 为了允许操作系统在线程退出时自动清理线程特定资源(TCB),因为我们明确指定我们对稍后加入线程不感兴趣,因此不能加入已分离的线程。

终止线程

进程终止时的行为与主线程的行为相同,主线程至少可以捕获一些信号。其他线程是否能够处理信号并不重要,因为可以在主线程的信号处理程序调用中加入或终止其他线程。 (相关问题)

正如已经提到的那样,大多数操作系统上,任何线程(无论是否分离)都将随其进程而死亡。可以通过引发信号、调用exit()或从主函数返回来终止进程本身。然而,C++11不能并且也不试图定义底层操作系统的确切行为,而Java虚拟机的开发人员可以在一定程度上抽象出这种差异。据我所知,奇特的进程和线程模型通常出现在古老的平台上(C++11可能不会被移植到这些平台),以及各种嵌入式系统中,这些系统可能具有特殊和/或有限的语言库实现和有限的语言支持。

线程支持

如果不支持线程,则std::thread::get_id()应返回无效ID(默认构造的std::thread::id),因为有一个普通进程,不需要线程对象来运行,而且std::thread的构造函数应该抛出std::system_error。这是我对C++11与今天的操作系统的理解。如果有一个支持线程但在其进程中没有生成主线程的操作系统,请告诉我。

控制线程

如果需要控制线程以进行适当的关闭,可以使用同步原语和/或某种标志来实现。然而,在这种情况下,设置一个关闭标志后跟随着 join 是我喜欢的方式,因为通过分离线程增加复杂性是没有意义的,因为资源将在任何情况下同时释放,其中 std::thread 对象的几个字节比更高的复杂性和可能更多的同步原语应该是可以接受的。

3
由于每个线程都有自己的堆栈(在Linux上一般为兆字节级别),我会选择分离该线程(这样它的堆栈将在退出时被释放),并使用一些同步原语,以便主线程需要退出时进行适当的关闭(而不是在返回/退出时终止仍在运行的线程, 需要加入这些线程才能正确关闭)。 - Norbert Bérci
14
我真的不明白这怎么回答了问题。 - MikeMB

33

请考虑以下代码:

#include <iostream>
#include <string>
#include <thread>
#include <chrono>

void thread_fn() {
  std::this_thread::sleep_for (std::chrono::seconds(1)); 
  std::cout << "Inside thread function\n";   
}

int main()
{
    std::thread t1(thread_fn);
    t1.detach();

    return 0; 
}

在Linux系统上运行它时,线程函数thread_fn的消息从未被打印出来。操作系统确实会在main()退出后立即清理thread_fn()。将t1.detach()替换为t1.join()总是按预期打印消息。


1
这种行为恰好发生在Windows上。因此,似乎当程序完成时,Windows会终止已分离的线程。 - TonySalimi
3
我尝试使用分离的线程和父进程退出后写入文件,以为可能是因为父进程完成后标准输出不起作用。但它也不会写入文件。所以你是对的。 - Ashish Aggarwal

22
程序退出后线程的命运是未定义行为。但是现代操作系统会在关闭进程时清理所有由该进程创建的线程。
当分离一个std::thread时,以下三个条件仍然成立:
  1. *this不再拥有任何线程
  2. joinable()始终等于false
  3. get_id()将等于std::thread::id()

2
为什么是未定义的?因为标准没有定义任何东西吗?根据我的脚注,这是否意味着对detach()的任何调用都具有未定义的行为?很难相信... - Marc Mutz - mmutz
2
@MarcMutz-mmutz 在这里的未定义是指,如果进程退出,线程的命运就不确定了。 - Caesar
3
@Caesar 我该如何确保在线程完成前不退出? - MichalH
2
@MichalH 你使用 join() - fllprbt
如果程序永远不退出,例如如果main在一个无限循环中完成工作并且永远不返回,你就可以避免 UB。 - undefined

9

3

当主进程终止时,该进程创建的所有工作线程也将被终止。因此,如果main()在它创建的一个分离线程完成执行之前返回,则该分离线程将被操作系统杀死。看下面这个例子:

void work(){
     this_thread::sleep_for(chrono::seconds(2));
     cout<<"Worker Thread Completed"<<endl;
}
int main(){
     thread t(work);
     t.detach();
     cout<<"Main Returning..."<<endl;
     return 0;
}

在上面的程序中,Worker Thread Completed永远不会被打印出来。因为main在工作线程的2秒延迟之前就返回了。现在,如果我们稍微改变一下代码,在main返回之前添加一个大于2秒的延迟。例如:
void work(){
     this_thread::sleep_for(chrono::seconds(2));
     cout<<"Worker Thread Completed"<<endl;
}
int main(){
     thread t(work);
     t.detach();
     cout<<"Main Returning..."<<endl;
     this_thread::sleep_for(chrono::seconds(4));
     return 0;
}

输出

Main Returning...
Worker Thread Completed

现在,如果从除了 main 函数以外的任何函数中创建线程,则已分离的线程将保持活动状态,直到其执行完成,甚至在函数返回后仍然是如此。例如:
void child()
{
     this_thread::sleep_for(chrono::seconds(2));
     cout << "Worker Thread Completed" << endl;
}
void parent(){
     thread t(child);
     t.detach();
     cout<<"Parent Returning...\n";
     return;
}
int main()
{
     parent();
     cout<<"Main Waiting..."<<endl;
     this_thread::sleep_for(chrono::seconds(5));
}

输出

Parent Returning...
Main Waiting...
Worker Thread Completed

使用condition_variable的一个解决方法,使得main在返回之前等待一个分离的工作线程。例如:
#include <bits/stdc++.h>
using namespace std;
condition_variable cv;
mutex m;
void work(){
    this_thread::sleep_for(chrono::seconds(2));
    cout << "Worker Thread Completed" << endl;
    cv.notify_all();
}
int main(){
    thread t(work);
    t.detach();
    cout << "Main Returning..." << endl;
    unique_lock<mutex>ul(m);
    cv.wait(ul);
    return 0;
}

1

为了让其他线程继续执行,主线程应该调用pthread_exit()而不是exit(3)来终止。 在主线程中使用pthread_exit是可以的。当使用pthread_exit时,主线程将停止执行,并保持僵尸(defunct)状态,直到所有其他线程退出。 如果在主线程中使用pthread_exit,则无法获得其他线程的返回状态,并且无法为其他线程进行清理(可以使用pthread_join(3)完成)。另外,最好分离线程(pthread_detach(3)),以便在线程终止时自动释放线程资源。共享资源直到所有线程退出才会被释放。


@kgvinod,为什么不在“ti.detach()”之后添加“pthread_exit(0);”呢? - yshi
参考:https://dev59.com/u3A65IYBdhLWcg3w7DT8将pthread_exit()从主函数调用是否可行? - yshi

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