在信号处理程序中显式调用析构函数

Posted

技术标签:

【中文标题】在信号处理程序中显式调用析构函数【英文标题】:Explicitly calling a destructor in a signal handler 【发布时间】:2009-11-13 21:19:42 【问题描述】:

我有一个析构函数执行一些必要的清理(它会杀死进程)。即使将 SIGINT 发送到程序,它也需要运行。我的代码目前看起来像:

typedef boost::shared_ptr<PidManager> PidManagerPtr
void PidManager::handler(int sig)

  std::cout << "Caught SIGINT\n";
  instance_.~PidManagerPtr();  //PidManager is a singleton
  exit(1);

//handler registered in the PidManager constructor

这可行,但似乎有很多警告不要显式调用析构函数。在这种情况下这是正确的做法,还是有“更正确”的方法?

【问题讨论】:

我希望你没有使用静态方法作为signa的回调。注意:C++ ABI 没有定义调用约定,因此您是在玩火。信号处理程序应声明为外部“C”函数。 【参考方案1】:

如果该对象是单例,则不需要使用共享指针。 (只有一个!)

如果你把它切换到auto_ptr,你可以在上面调用release()。或者scoped_ptr,打电话给reset()

这一切都表明,我 99% 确定 exit() 将破坏静态构造的对象。 (往往是单身人士。)我所知道的是exit() 调用已注册的atexit() 函数。

如果您的单例没有通过退出自动销毁,那么在您的情况下,正确的做法是创建一个 atexit 挂钩:

void release_singleton(void)

    //instance_.release();
    instance_.reset();


// in main, probably
atexit(release_singleton);

【讨论】:

多个类可以有自己的共享指针到 PidManager 实例。当然,如果发生这种情况,我的代码只会将引用计数减一并且它不会工作。巧合的是,在开发的这个阶段,只有一个类实际上拥有一个 PidManager。我必须解决这个问题。谢谢。我会在星期一试用 atexit()。 关于 atexit() 的特别说明:“如果进程因传递信号而异常终止,则不会调用使用 atexit() 注册的函数。”也许我误解了答案,但是注册 atexit() 处理程序并不能消除对信号处理程序的需要。【参考方案2】:

永远不要显式调用析构函数,除非对象是用放置 new 构造的。 将清理代码移动到单独的函数中并调用它。从析构函数中调用相同的函数。

【讨论】:

显然在可以再次调用析构函数的情况下显式调用析构函数是不好的,但如果我在下一行退出似乎很安全。会发生什么我不应该这样做的坏事? @Dan Hook:请记住,您对信号处理中的线程状态知之甚少。对象状态可能完全不连贯——它可能在该对象的方法中。 显式调用析构函数很少是个好主意。它可能是安全的(在这种特殊情况下),甚至可能是合理的(如果您想在销毁对象后立即使用放置新的内存来重用内存),但不要忘记将来必须维护您的代码的程序员。我们在这里看到的是“上下文意外”的示例,即代码很糟糕,但它可以工作,因为程序退出了下一行。 “碰巧编程”是个坏习惯,不要养成。【参考方案3】:

事实证明,这样做是一个非常糟糕的主意。发生的奇怪事情的数量是巨大的。

发生了什么

shared_ptr 进入处理程序的 use_count 为 2。一个引用在 PidManager 本身中,另一个在 PidManager 的客户端中。调用 shared_ptr (~PidManager() ) 的析构函数将 use_count 减一。然后,正如 GMan 所暗示的,当调用 exit() 时,静态初始化的 PidManagerPtr instance_ 的析构函数被调用,将 use_count 减少到 0 并导致 PidManager 析构函数被调用。显然,如果 PidManager 有多个客户端,use_count 就不会下降到 0,这根本不会起作用。

这也提供了一些关于为什么调用 instance_.reset() 不起作用的提示。该调用确实将引用计数减少了 1。但剩余的引用是 PidManager 客户端中的 shared_ptr。 shared_ptr 是一个自动变量,因此它的析构函数不会在 exit() 中调用。调用了 instance_ 析构函数,但由于它被 reset(),它不再指向 PidManager 实例。

解决方案

我完全放弃了 shared_ptrs 的使用,并决定改用 Meyers Singleton。现在我的代码如下所示:

void handler(int sig)

     exit(1);


typedef PidManager * PidManagerPtr
PidManagerPtr PidManager::instance()

    static PidManager instance_;
    static bool handler_registered = false;
    if(!handler_registered)
    
        signal(SIGINT,handler);
        handler_registered = true;
    
    return &instance_;
 

显式调用 exit 允许静态初始化的 PidManager instance_ 的析构函数运行,因此无需在处理程序中放置其他清理代码。这巧妙地避免了在 PidManager 处于不一致状态时调用处理程序的任何问题。

【讨论】:

【参考方案4】:

您真的不想在信号处理程序中做很多事情。最安全的做法是设置一个标志(例如,一个全局 volatile bool),然后让程序的常规事件循环每隔一段时间检查该标志,如果它变为真,则从那里调用清理/关闭例程。

由于信号处理程序与应用程序的其余部分异步运行,因此在信号处理程序内部执行的操作比在信号处理程序内部执行的操作更多是不安全的——您可能想要与之交互的任何数据都可能处于不一致的状态。 (而且你也不允许使用互斥锁或来自信号处理程序的其他同步——这样的信号非常邪恶)

但是,如果您不喜欢一直轮询布尔值的想法,您可以在信号处理程序(至少在大多数操作系统上)中做的另一件事是在套接字上发送一个字节。因此,您可以提前设置一个 socketpair(),并在套接字对的另一端设置正常的事件循环 select() (或其他);当它在该套接字上接收到一个字节时,它知道您的信号处理程序一定已经发送了该字节,因此是时候清理了。

【讨论】:

【参考方案5】:

另一种方法是动态分配单例(在首次使用时或在主中),并delete 进行清理。

嗯。我猜你的 PidManagerPtr 实际上指向一个动态分配的对象......但是 boost::shared_ptr 实际上并没有在重新分配时清理吗?所以应该足够了:

instance_ = 0;

?

【讨论】:

好吧,instance_ = 0 无法编译。 instance_.reset((PidManager*)NULL) 也不起作用。 等一下,你不能那样做。信号处理程序绝对可以随时运行。不是操作堆的最佳机会。【参考方案6】:

只需在 shared_ptr 上调用 reset(),它就会为您删除您的实例。

【讨论】:

你确定吗?见***.com/questions/156373/…

以上是关于在信号处理程序中显式调用析构函数的主要内容,如果未能解决你的问题,请参考以下文章

C++ 堆栈分配对象,显式析构函数调用

如何在 GDAL ruby​​ 绑定中显式关闭数据集?

是否允许显式调用析构函数,然后在具有固定生命周期的变量上放置 new?

C++ 类设计总结回顾------析构函数

显式调用析构函数

9——对象的创建和撤销,构造函数和析构函数