6.5 快速退出:quick_exit与at_quick_exit

类别:所有人

在C++程序中,我们常常会看到一些有关“终止”的函数,如terminate、abort、exit等。这些函数容易让人产生疑惑,因为对于普通的程序来说,它们都只是终止程序的运行而已。不过实际上它们还是有很大的区别的。因为其对应的是“正常退出”和“异常退出”两种情况。

首先我们可以看看terminate函数,terminate函数实际上是C++语言中异常处理的一部分(包含在<exception>头文件里)。一般而言,没有被捕捉的异常就会导致terminate函数的调用。此外我们在第2章中提到过的noexcept关键字声明的函数,如果抛出了异常,也会调用terminate函数。其他还有很多的情况。但直观地讲,只要C++程序中出现了非程序员预期的行为,都有可能导致terminate的调用。而terminate函数在默认情况下,是去调用abort函数的。不过用户可以通过set_terminate函数来改变默认的行为。因此,可以认为在C++程序的层面,termiante就是“终止”。

相对于termiante,源自于C中(头文件<cstdlib>)的abort则更加低层。abort函数不会调用任何的析构函数(读者也许想到了,默认的terminate也是如此),默认情况下,它会向合乎POSIX标准的系统抛出一个信号(signal):SIGABRT。如果程序员为信号设定一个信号处理程序的话(signal handler),那么操作系统将默认地释放进程所有的资源,从而终止程序。可以说,abort是系统在毫无办法下的下下策——终止进程。有时候这会带来一些问题。典型的,倘若被终止的应用程序进程与其他应用程序软件层有一些交互(比如一些硬件驱动程序,一些通过网络通信的程序等,假设这些程序设计得并不那么健壮),那么本进程的意外终止,都可能导致这些交互进程处于一些“中间状态”,进而出现一些问题。

相比而言,exit这样的属于“正常退出”范畴的程序终止,则不太可能有以上的问题。exit函数会正常调用自动变量的析构函数,并且还会调用atexit注册的函数。这跟main函数结束时的清理工作是一样的。我们可以看看代码清单6-28所示的例子。

代码清单6-28


include <cstdlib>

include <iostream>

using namespace std;

void openDevice(){cout<<"device is opened."<<endl;}

void resetDeviceStat(){cout<<"device stat is reset."<<endl;}

void closeDevice(){cout<<"device is closed."<<endl;}

int main(){

atexit(closeDevice);

atexit(resetDeviceStat);

openDevice();

exit(0);

}

编译选项:g++6-5-1.cpp


在代码清单6-28中,我们使用atexit注册了两个函数:resetDeviceStat和closeDevice。编译运行该例子后,程序的输出如下:


device is opened.

device stat is reset.

device is closed.


可以看到,在程序退出时(调用ANSI C定义的exit函数的时候),所有注册的函数都被调用,值得注意的是,注册的函数被调用的次序与其注册顺序相反,这多少跟析构函数的执行与其声明的顺序相反是一致的。exit和atexit函数同样来自于C,通过两者的配合,我们可以灵活地处理一些进程级的清理工作,这对一些静态、全局变量来说,是非常有用的。

不过有的时候,main或者使用exit函数调用结束程序的方式也不那么令人满意。有的时候,代码中会有很多的类,这些类在堆空间上分配了大量的零散的内存(直接从堆里分配,并没有优化的策略),而main或者exit函数调用会导致类的析构函数依次将这些零散的内存还给操作系统。这是一件费时的工作,而实际上,这些堆内存将在进程结束时由操作系统统一回收(事实上这相当快,操作系统除了释放一些进程相关的数据结构外,只是将一些物理内存标记为未使用就可以了)。如果这些堆内存对其他程序不产生任何影响,那么在程序结束时释放堆内存的析构过程往往是毫无意义的。因此,在这种情况下,我们常常需要能够更快地退出程序。

另外,在多线程情况下,我们要使用exit函数来退出程序的话,通常需要向线程发出一个信号,并等待线程结束后再执行析构函数、atexit注册的函数等。这从语法上讲非常正确,不过这样的退出方式有的时候并不能够像预期那样工作,比如说线程中的程序在等待I/O运行结束等。在一些更为复杂的情况下,可能还会遭遇到一些因为信号顺序而导致的死锁状况。一旦出现了这样的问题,程序往往就会被“卡死”而无法退出。

为此,在C++11中,标准引入了quick_exit函数,该函数并不执行析构函数而只是使程序终止。与abort不同的是,abort的结果通常是异常退出(可能系统还会进行coredump等以辅助程序员进行问题分析),而quick_exit与exit同属于正常退出。此外,使用at_quick_exit注册的函数也可以在quick_exit的时候被调用。这样一来,我们同样可以像exit一样做一些清理的工作(这与很多平台上使用_exit函数直接正常退出还是有不同的)。在C++11标准中,at_quick_exit和at_exit一样,标准要求编译器至少支持32个注册函数的调用。代码清单6-29所示是一个可能能够运行的例子。

代码清单6-29


include <cstdlib>

include <iostream>

using namespace std;

struct A{~A(){cout<<"Destruct A."<<endl;}};

void closeDevice(){cout<<"device is closed."<<endl;}

int main(){

A a;

at_quick_exit(closeDevice);

quick_exit(0);

}


这里我们定义了一个类型A的变量a,以及注册了一个quick_exit调用的函数closeDevice。如果示例正确的话,变量a的析构函数将不会被调用。