单例销毁必须按场景选机制:Windows DLL用DllMain响应DLL_PROCESS_DETACH并判别显式卸载;Linux SO需显式shutdown()配合__attribute__((destructor))兜底;主程序优先用Meyer单例(函数内static局部变量);所有析构仅操作本类原始资源,禁用外部依赖。

单例在程序退出或动态库卸载时没销毁,不是忘了写析构函数,而是生命周期管理方式错了——堆上分配的 static Singleton* 根本不会自动析构;而函数内 static Singleton instance 虽能自动析构,但在 DLL/SO 中常被跳过。必须按场景选对机制。
Windows DLL 卸载时必须用 DllMain 响应 DLL_PROCESS_DETACH
这是唯一可控的方式。系统只在 FreeLibrary 主动卸载时触发该事件,但需靠 lpReserved == nullptr 判定是否为显式卸载(排除进程退出)。否则直接调用 destroyInstance() 可能访问已卸载的 CRT 或其他模块符号,引发崩溃。
-
destroyInstance()必须用std::atomic<bool></bool>标记已销毁状态,防止多次调用 - 销毁前先处理非平凡资源:
thread.join()、close(fd)、munmap(),不能依赖析构函数隐式释放 - 销毁后立即将内部指针置为
nullptr,并在getInstance()中检查并抛出std::runtime_error("singleton destroyed") - 禁止在
DllMain中调用LoadLibrary、CreateThread或任何可能加载/初始化其他模块的 API
Linux SO 卸载时不能只靠 __attribute__((destructor))
这个属性注册的函数会在 dlclose 后执行,但它不区分是 dlclose 还是进程退出触发,也不保证执行顺序。若单例析构时调用了已被卸载的日志库或内存池,SEGFAULT 几乎必然发生。
- 定义一个 C 风格函数,如
static void so_cleanup() { Singleton::destroyInstance(); },并在其后加__attribute__((destructor)) -
destroyInstance()内部必须先检查std::thread::joinable()再join(),对std::mutex用try_lock()避免死锁 - 编译时禁用
-fvisibility=hidden,否则该函数可能被链接器剥离 - 它只能作兜底,主清理路径必须是宿主在
dlclose前显式调用Singleton::shutdown()
程序退出时优先用 Meyer 单例(函数内静态局部变量)
这是主程序中唯一推荐的自动析构方案。C++11 保证 static Singleton instance 的析构发生在 main() 返回后,且不参与跨编译单元的全局析构序列,彻底规避顺序问题。
立即学习“C++免费学习笔记(深入)”;
- 返回类型必须是引用(
Singleton&),不能返回指针;否则又退化为裸指针管理 - 所有依赖项(如其他单例)也必须用同样方式实现,否则析构时仍可能访问已销毁对象
- 析构函数中只做轻量清理:关闭文件描述符、解除映射、通知监听器;不要调用日志、网络、数据库等高层接口
- 若构造函数里调用了其他单例,则该单例的析构会晚于当前单例——注意依赖方向
跨平台或高可靠性场景必须提供显式 shutdown() 接口
当单例持有数据库连接、监听套接字、线程池等关键资源时,把销毁时机完全交给宿主是最稳妥的做法。自动析构永远存在不确定性,尤其在多线程、信号处理、快速退出(_Exit)等边界场景下。
-
shutdown()必须幂等:开头用std::atomic检查是否已执行,避免重复delete - 销毁前加锁(如果
getInstance()支持并发访问),确保与后续调用互斥 - 销毁后将指针置空,并在
getInstance()中检查,拒绝后续访问 - 宿主程序必须严格配对:
dlopen→init_singleton()→ 使用 →destroy_singleton()→dlclose
最易被忽略的一点是:即使你用了所有正确机制,只要析构函数里调用了任何外部单例、标准库流对象(如 std::cout)或第三方库函数,就可能在卸载阶段因符号不可用而崩溃。安全边界只有一条——析构函数只操作本类直接持有的原始资源,且全部判空。


















