C++ 优雅关闭最佳实践

11

我正在为*nix操作系统编写多线程C++应用程序。如何优雅地终止这样一个应用程序?我的想法是安装一个SIGINT(SIGTERM?)信号处理程序,停止/联结我的线程。同时,是否可能“保证”在处理信号时调用所有析构函数(前提是没有其他错误或异常抛出)?


好问题...我自己也很好奇答案是什么。 - FuzzyBunnySlippers
我认为使用全局关机标志不是个坏主意,你可以在主事件循环或者其它通常用于同步的地方进行检查。为了保证所有析构函数都被调用,需要展开每个线程的调用栈,假设你为堆分配的对象使用RAII。对此问题并没有什么万能解决方法,最简单的方法是抛出一个异常,并在线程的主函数中捕获它。 - Alexei Averchenko
3个回答

4

有几点需要考虑:

  • 指定一个线程来负责协调关闭,例如,正如Dithermaster所建议的那样,如果您正在编写独立应用程序,则可以将其设置为主线程。或者,如果您正在编写库,则提供接口(例如函数调用),使客户端程序可以终止在库中创建的对象。

  • 您无法保证会调用析构函数;这取决于您,需要仔细地为每个new调用删除。也许智能指针可以帮助您。但是,这确实是一个设计问题。主要组件应具有启动和停止语义,您可以选择从类构造函数和析构函数中调用它们。

  • 一组交互对象的关闭序列是可能需要一些努力才能正确获取的。例如,在删除对象之前,您确定没有计时器机制会在几微/毫/秒后尝试调用它吗?在此,尝试和错误是您的朋友;开发一个可以重复快速启动和停止应用程序以解决关闭相关竞争条件的框架。

  • 信号是触发事件的一种方式;其他方法可能是周期性轮询已知文件,或打开套接字并在其上接收某些数据。无论哪种方式,您都希望将关闭序列代码与触发事件分离。


2

我的建议是主线程在退出之前关闭所有工作线程。向每个工作线程发送一个事件,告诉它清理并退出,并等待每个工作线程执行完毕。这将允许所有C++析构函数运行。


1
关于信号管理,在信号处理程序中,你唯一能够可移植和安全地做的事情就是写入一个 sig_atomic_t 类型的变量(可能带有 volatile 限定符)并返回。通常情况下你不能调用大多数函数,也必须不写入全局内存。换句话说,处理程序应该只设置一个标志,在适当的时候在主程序中进行测试,并从那里执行由信号本身导致的操作。
(因为可能涉及阻塞 I/O,请考虑学习 POSIX Thread Cancellation。你的 Unix 克隆(尤其是 Linux)可能存在与此相关的特殊情况。)
关于析构函数,不存在什么魔法。如果控制通过语言中定义的任何方式离开给定作用域,它们将被执行。通过其他方式离开作用域(例如,longjmp() 或甚至是 exit())不会触发析构函数。

关于通用的关闭程序实践,在该领域存在不同的观点。

有些人认为应该执行“优雅终止”,即释放所有已分配的资源。在C++中,这通常意味着在进程终止之前必须正确执行所有析构函数。在实践中,这很棘手,而且常常是多线程程序的主要问题之一,原因有很多。信号通过异步信号分发的本质进一步复杂化了事情。

因为大部分工作都是完全无用的,像我这样的其他人认为程序必须立即终止,可能在撤消对系统的持久更改(如删除临时文件或恢复屏幕分辨率)并保存配置后不久。一个表面上更整洁的清理不仅是浪费时间的(因为操作系统将清理大部分像分配的内存、悬空线程和打开的文件描述符之类的东西),而且可能是一个严重的时间浪费(解除分配器可能会触及页出内存,无用地迫使系统将它们分页以便在进程终止后不久释放它们,例如),更不用提由加入线程引起死锁的可能性。

只需说不。当你想离开时,调用exit()(甚至是_exit(),但要注意未刷新的I/O),就这样。比启动慢的程序更烦人的是终止缓慢的程序。


在其他线程中“raise”信号也是安全的吗?signal handler 中也允许使用 longjmp,可能还包括 throw - Ben Voigt
@BenVoigt:当然,这就是我说“一般情况”的原因。可以调用任何信号安全函数,但根据POSIX.1的规定(在SUSv2中,在“signal()”的文档条目下也是如此),如果处理程序引用除写入“sig_atomic_t”(或在后续修订版中使用“errno”)之外的静态内存,则行为是未定义的。这使得几乎不可能以有用的方式调用大多数函数,包括pthread_kill()longjmp(),显而易见的原因。最后,我不了解在多线程POSIX应用程序中,信号处理程序内部使用throw时的语义。 - alecov

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