为什么`std::exit`不能按预期触发析构函数?

25
#include <cstdlib>
#include <thread>
#include <chrono>
#include <iostream>

using namespace std;
using namespace std::literals;

struct A
{
    int n_ = 0;
    A(int n) : n_(n) { cout << "A:" << n_ << endl; }
    ~A() { cout << "~A:" << n_ << endl; }
};

A a1(1);

int main()
{
    std::thread([]()
    {
        static A a2(2);
        thread_local A a3(3);
        std::this_thread::sleep_for(24h);
    }).detach();

    static A a4(4);
    thread_local A a5(5);

    std::this_thread::sleep_for(1s);
    std::exit(0);
}

我的编译器是 clang 5.0,使用了 -std=c++1z 标准。

输出如下:

A:1
A:2
A:4
A:5
A:3
~A:5
~A:2
~A:4
~A:1

请注意,没有~A:3,这意味着对象A a3未被销毁。

然而,根据cppref

std::exit导致正常程序终止。执行几个清理步骤:

具有线程局部存储期的对象的析构函数...得到保证被调用。


因为你“分离”它了,我猜。 - SingerOfTheFall
1
这是 https://dev59.com/NWIj5IYBdhLWcg3w8JRZ 的重复吗? - François Andrieux
1
不,即使线程没有分离,情况也是一样的。 - xmllmx
4
当然,这假设所有析构函数仅执行简单的内存管理操作,并且不执行任何更复杂的操作。但是,如果退出时没有显式调用析构函数,则会破坏std::basic_fstream的刷新保证(例如),因为刷新行为是由其成员std::basic_filebuf的析构函数引起的。 - Justin Time - Reinstate Monica
1
一般来说,没有办法从外部干净地终止一个线程。必须由线程本身(包括调用线程退出作为它所做的最后一件事情)调用您的(或库、编译器运行时)代码。换句话说,您必须为每个线程编写线程退出逻辑。 - hyde
显示剩余8条评论
2个回答

39

具有线程存储期的对象仅保证在调用exit的线程中被销毁。引用C++14(N4140),[support.start.term] 18.5/8(强调我的):

[[noreturn]] void exit(int status)

exit()函数在国际标准中有额外的行为:

  • 首先,与当前线程关联的具有线程存储期限的对象将被销毁。接下来,静态存储期限的对象将被销毁,并调用通过调用atexit注册的函数。有关销毁和调用顺序,请参见3.6.3。(调用exit()不会导致自动对象被销毁。) 如果控制离开由exit调用的已注册函数,因为该函数没有为抛出的异常提供处理程序,则必须调用std::terminate()(15.5.1)。
  • 接下来,所有具有未写入缓冲数据的打开C流(由<cstdio>中声明的函数签名进行介质化)都将被刷新,所有打开的C流都将被关闭,并且通过调用tmpfile()创建的所有文件都将被删除。
  • 最后,控制将返回到主机环境。如果状态为零或EXIT_SUCCESS,则返回实现定义的成功终止状态的形式。如果状态为EXIT_FAILURE,则返回实现定义的未成功终止状态的形式。否则,返回的状态是实现定义的。

因此,该标准不保证销毁与调用exit的线程不同的其他线程关联的具有线程存储期限的对象。


10
这意味着cppreference(在问题中引用)与C++规范不一致;应该在该网站上报告此错误。 - Hans Olsson
5
@HansOlsson说:cppreference.com是一个维基网站,因此,如果您愿意,您可以自行修复问题,而不必将其报告为漏洞。 :-) - ruakh

16

问题在于当你退出进程时,线程会被(在大多数现代多任务操作系统上)强制终止。这种终止线程的方式发生在操作系统层面,而操作系统对对象或析构函数一无所知。


3
很抱歉,我认为这完全没有回答问题。std::exit 在退出进程前会进行一些清理工作,这正是 OP 所问的清理工作。没有人期望在进程退出后调用析构函数。 - ruakh
2
std::exit不是操作系统级别的函数或者系统调用,它是C++标准库中的一个函数。因此,它将会意识到C++语言结构。无论操作系统是否知道对象或者是否存在操作系统都是无关紧要的。 - josefx

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