【问题标题】:What happens to a detached thread when main() exits?当 main() 退出时,分离的线程会发生什么?
【发布时间】:2017-06-12 12:28:02
【问题描述】:

假设我开始一个std::thread 然后detach() 它,所以线程继续执行,即使曾经代表它的std::thread 超出范围。

进一步假设程序没有可靠的协议来加入分离线程1,所以当main()退出时分离线程仍然运行。

我在标准中找不到任何内容(更准确地说,在 N3797 C++14 草案中),它描述了应该发生的事情,1.10 和 30.3 都没有包含相关的措辞。

1 另一个可能等效的问题是:“分离的线程是否可以再次加入”,因为无论您发明什么协议来加入,信号部分都必须在线程仍在运行,并且操作系统调度程序可能决定在执行信号后立即让线程休眠一个小时,而接收端无法可靠地检测到线程实际上已完成。

如果在运行分离线程的情况下用完main() 是未定义的行为,那么任何使用std::thread::detach() 都是未定义的行为,除非主线程永远不会退出2

因此,在运行分离线程的情况下用完main() 必须具有已定义 效果。问题是:在哪里(在 C++ 标准中,不是 POSIX,不是 OS 文档,...)是那些定义的效果。

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

【问题讨论】:

  • 我只能在 [basic.start.term]/4 中找到一个非常模糊的非强制性注释:“在调用std::exit 或退出main 之前终止每个线程就足够了,但不是必要的,以满足这些要求。” (整段可能相关)另见 [support.start.term]/8(main 返回时调用std::exit

标签: c++ multithreading c++11 exit stdthread


【解决方案1】:

程序退出后线程的命运是未定义的行为。但是现代操作系统会在关闭进程时清理进程创建的所有线程。

分离std::thread 时,这三个条件将继续成立:

  1. *this 不再拥有任何线程
  2. joinable() 将始终等于 false
  3. get_id() 将等于 std::thread::id()

【讨论】:

  • 为什么未定义?因为标准没有定义任何东西?根据我的脚注,对detach() 的任何调用不会有未定义的行为吗?难以置信……
  • @MarcMutz-mmutz 它是未定义的,如果进程退出,线程的命运是未定义的。
  • @Caesar 以及如何确保在线程完成之前不退出?
  • @MichalH 你使用join()
【解决方案2】:

分离线程

根据std::thread::detach

将执行线程与线程对象分开,允许 执行以独立继续。任何分配的资源将 线程退出后释放。

来自pthread_detach

pthread_detach() 函数应指示实现 当该线程时,可以回收该线程的存储空间 终止。如果线程尚未终止,则 pthread_detach() 不应 导致它终止。多次 pthread_detach() 调用的效果 在同一目标线程上未指定。

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

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

杀死线程

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

如前所述,any thread, whether detached or not, will die with its process on most OSes。进程本身可以通过发出信号、调用exit() 或从主函数返回来终止。但是,C++11 不能也不会尝试定义底层操作系统的确切行为,而 Java VM 的开发人员肯定可以在一定程度上抽象出这些差异。 AFAIK、奇异进程和线程模型通常出现在古老的平台(C++11 可能不会移植到这些平台)和各种嵌入式系统上,这些系统可能具有特殊和/或有限的语言库实现以及有限的语言支持。

线程支持

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

控制线程

如果需要保持对线程的控制以正确关闭,可以通过使用同步原语和/或某种标志来实现。但是,在这种情况下,我更喜欢在连接后设置关闭标志,因为通过分离线程来增加复杂性是没有意义的,因为无论如何都会同时释放资源,其中@的几个字节987654331@ 对象与更高的复杂性和可能更多的同步原语应该是可以接受的。

【讨论】:

  • 由于每个线程都有自己的堆栈(在 Linux 上是兆字节范围内),我会选择分离线程(因此它的堆栈将在它退出后立即被释放)并使用一些同步如果主线程需要退出(为了正确关闭,它需要加入仍在运行的线程,而不是在返回/退出时终止它们)。
  • 我真的不明白这是如何回答问题的
【解决方案3】:

原始问题“main() 退出时分离线程会发生什么”的答案是:

它继续运行(因为标准并没有说它停止),而且这是明确定义的,只要它既不接触其他线程的 (automatic|thread_local) 变量也不接触静态对象。

这似乎允许线程管理器作为静态对象([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 函数加入一个分离的线程来执行信号 使分离的线程执行对信号处理程序也是安全的代码。

【讨论】:

  • 你确定吗?在我测试的所有地方(GCC 5、clang 3.5、MSVC 14),当主线程退出时,所有分离的线程都会被杀死。
  • 我认为问题不在于具体实现的作用,而在于如何避免标准定义为未定义的行为。
  • 这个答案似乎暗示在破坏静态变量后,进程将进入某种睡眠状态,等待任何剩余的线程完成。这不是真的,在exit 完成销毁静态对象、运行atexit 处理程序、刷新流等之后,它将控制权返回给主机环境,即进程退出。如果一个分离的线程仍在运行(并且通过不接触它自己的线程之外的任何东西以某种方式避免了未定义的行为),那么它会随着进程退出而消失。
  • 如果您可以使用非 ISO C++ API,那么如果 main 调用 pthread_exit 而不是返回或调用 exit,那么这将导致进程等待分离的线程完成,然后在最后一个完成后调用exit
  • "它继续运行(因为标准没有说它停止)" --> 谁能告诉我一个线程如何在它的容器进程中继续执行?
【解决方案4】:

考虑以下代码:

#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() 始终会按预期打印消息。

【讨论】:

  • 这种行为完全发生在 Windows 上。因此,Windows 似乎在程序完成时杀死了分离的线程。
  • 我尝试使用分离的线程和父级退出写入文件,认为父级完成后可能是标准输出不起作用。但它也不会写入文件。所以你是对的。
【解决方案5】:

当主线程(即运行 main() 函数的线程)终止时,进程终止,所有其他线程停止。

参考:https://stackoverflow.com/a/4667273/2194843

【讨论】:

    【解决方案6】:

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

    【讨论】:

    【解决方案7】:

    当主进程终止时,该进程创建的所有工作线程也会被杀死。因此,如果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
    

    使main 在返回之前等待分离的工作线程的解决方法是使用condition_variable。例如:

    #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;
    }
    

    【讨论】:

      猜你喜欢
      • 2019-02-11
      • 2017-02-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-12-12
      相关资源
      最近更新 更多