动态库中单例析构不可靠,主因是系统不保证库卸载时调用静态对象析构函数;应改用显式init/destroy接口,由宿主程序严格控制生命周期。

程序退出时单例没析构,不是漏写了 ~Singleton(),而是对象根本不在静态存储期;动态库卸载时单例残留,也不是析构函数写得不好,而是系统压根不保证调用它。
static Singleton& getInstance() { static Singleton instance; } 在主程序里安全,在 .so/.dll 里不安全
这个写法在可执行文件中靠 __cxa_atexit 注册析构,main() 返回后必执行。但在动态库中,dlclose() 或 FreeLibrary() 不触发其析构——glibc 和 Windows CRT 常直接跳过整个静态对象清理阶段,尤其当 Singleton 析构函数里调用了日志、线程、锁等跨模块符号时,崩溃几乎是必然的。
- 现象包括:访问悬垂
std::mutex、std::string成员崩溃,或close()调用失败导致句柄泄漏 - 根本原因不是代码 bug,是 C++ 标准只保证“程序退出”时析构,不保证“库卸载”时析构
- 不要试图用
atexit()补救:它注册的函数在进程退出才跑,此时动态库代码段可能已unmapped
Windows DLL 必须用 DllMain 响应 DLL_PROCESS_DETACH
这是唯一可控的方式:在 DllMain 的 DLL_PROCESS_DETACH 分支中执行销毁,且只响应 FreeLibrary 主动卸载,排除进程退出场景。
- 必须检查
lpReserved == nullptr,否则会在进程终止时误触发销毁逻辑 -
destroyInstance()内部要用std::atomic<bool></bool>标记已销毁状态,防止多次调用 - 销毁前先处理非平凡资源:
thread.join()、fd显式close()、mutex.unlock()(或直接放弃) -
getInstance()开头加空指针检查:if (!m_instance) throw std::runtime_error("singleton destroyed");
Linux SO 必须用 __attribute__((destructor)) 配显式 shutdown 接口
编译器扩展 __attribute__((destructor)) 可在 dlclose() 后自动运行清理函数,但它执行顺序不可控、不区分卸载来源,只能作兜底,不能当主力。
立即学习“C++免费学习笔记(深入)”;
- 定义一个 C 风格全局函数,如
static void so_cleanup() { Singleton::shutdown(); },并加上该属性 -
shutdown()必须幂等:用std::atomic判空 + 加锁,避免多线程重复 delete - 宿主程序必须在
dlclose()前主动调用一次Singleton::shutdown(),这才是主清理路径 - 禁止在 destructor 函数中调用其他共享库导出的符号(如
Logger::get().log()),否则极易 SEGFAULT
最稳妥方案:禁用动态库内所有静态局部变量单例,改用显式 init/destroy 接口
把单例彻底移出静态存储期,只保留裸指针 + C 风格导出函数,由宿主程序严格配对控制生命周期。
- 导出两个
extern "C"函数:init_singleton()和destroy_singleton() -
init_singleton()内部用new分配实例,并用std::atomic<bool></bool>防止重复初始化 -
destroy_singleton()内部delete实例、置空指针,且必须在dlclose()前被显式调用 - 单例析构函数里只做最底层清理:判空后
close(fd)、munmap(),不调用任何高层模块(日志、网络、配置)
真正危险的不是“怎么写析构函数”,而是假设“系统会替你调用它”。动态库里的单例一旦依赖外部符号或跨模块资源,它的析构时机就永远不可预测——能靠得住的只有宿主程序那一行明确的 destroy_singleton() 调用。


















