单例对象在动态库卸载时几乎从不安全析构,因其依赖的CRT堆、线程池等在进程终止时已失效;Windows需通过DllMain中if(lpReserved==nullptr)判断是否为主动卸载,Linux应避免__attribute__((destructor))执行复杂逻辑,而将清理交由宿主显式调用extern "C" shutdownSingleton()完成。

动态库卸载时,单例对象几乎从不安全析构——这不是配置问题,而是 C++ 标准不保证、运行时不执行、操作系统跳过的确定性行为。依赖 static Singleton instance; 或 static Singleton* instance = new Singleton(); 在 .so/.dll 中,等于默认开启崩溃倒计时。
Windows 下 DllMain 的 DLL_PROCESS_DETACH 必须判空 lpReserved
DLL_PROCESS_DETACH 回调看似是卸载入口,但进程退出时也会触发它,此时 CRT 堆、线程池、全局 mutex 都已失效,直接调用 destroyInstance() 会 crash。
关键判断只有一行:if (lpReserved == nullptr)。
- 只有该条件为 true,才表示由 FreeLibrary 主动卸载,此时 CRT 和本模块资源仍可用
- 若 lpReserved != nullptr,说明是进程终止,应跳过所有销毁逻辑,让系统收尾
- destroyInstance() 内部必须用 std::atomic<bool></bool> 标记已销毁,防止多线程重复 delete
- 销毁后立即将静态指针置为 nullptr,并在 getInstance() 开头加空指针检查,避免后续误用
Linux 下 __attribute__((destructor)) 不能当主力,只能做兜底
__attribute__((destructor)) 函数在 dlclose 后执行,但它运行在“符号已卸载、STL 不可用、malloc 可能失效”的上下文中:
- 禁止在其中调用 std::cout、log()、std::mutex::unlock() 等任何跨模块或非 trivial 操作
- 它唯一安全用途是:检查 instance != nullptr,若为真,说明宿主忘了调用 shutdownSingleton(),此时最多用 write(2) 打印一行纯 ASCII 日志,或调用 abort() 提醒
- 真正的清理逻辑必须放在显式导出的 shutdownSingleton() 函数里,由宿主程序在 dlclose() 前调用
- 编译时若启用了 -fvisibility=hidden,需对 destructor 函数加 __attribute__((visibility("default"))),否则可能被剥离
显式 shutdown 接口必须是 extern "C" + 无副作用
这是跨平台最可靠的方式,把控制权完全交还给宿主:
- 导出函数签名必须是 extern "C" void shutdownSingleton();,避免 C++ name mangling 和 ABI 陷阱
- 函数体内只做三件事:加锁(用 std::atomic 或轻量自旋锁)、delete instance;、instance = nullptr;
- 禁止调用任何外部 API:不写日志、不关数据库连接、不发网络请求——这些高层逻辑应在 shutdown 调用前由宿主协调完成
- 若单例持有 std::thread,必须先 joinable() 判断再 join();若持 int fd,必须 if (fd != -1) close(fd);
- 宿主调用顺序不可乱:dlopen() → initSingleton() → 使用 → shutdownSingleton() → dlclose(),漏一步就泄漏或二次析构
真正危险的不是“怎么写销毁函数”,而是以为“写了就安全了”。<strong>attribute</strong>((destructor)) 和 DllMain 都只是钩子,它们的执行环境比你想象中更贫瘠、更不可控。唯一能托付信任的,是宿主程序在明确知道所有使用者已停止访问后,主动按下那个 shutdownSingleton() 按钮。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











