动态库卸载时单例对象几乎从不安全析构,因 dlclose/FreeLibrary 不保证静态析构函数执行;Meyer 单例在 .so/.dll 中必出问题,会导致 SEGFAULT 或析构崩溃;必须由宿主程序显式控制销毁时机。

动态库卸载时,单例对象几乎从不安全析构——这不是配置问题,是操作系统和运行时对 dlclose / FreeLibrary 的明确定义行为。你不能依赖 static Singleton instance; 或 static Singleton* 的静态析构函数被调用,也不能假设 std::shared_ptr 或 thread_local 能兜底。
必须把销毁时机完全交到宿主程序手上,其他所有“自动”方案都只是补充或兜底。
为什么 Meyer 单例在 .so/.dll 里一定会出问题
现象:`dlclose()` 后主程序再调用 `getInstance()`,访问已释放内存,直接 `SEGFAULT`;或者崩溃在 `~Singleton()` 里调用 `std::cout
-
static Singleton instance;的析构由 glibc/MSVCRT 在模块卸载时尝试触发,但dlclose不保证执行它(glibc 多数情况跳过) - 即使触发了,析构顺序不可控:`Singleton` 可能比它内部用的
std::mutex、std::string、甚至std::cout先析构 - 若单例持有
std::thread,线程未join()就退出,导致资源泄漏或进程级崩溃 - 插件系统中多个模块共享该单例指针,A 库卸载后 B 库仍解引用 → 悬垂指针无法调试
必须用裸指针 + std::call_once + 显式 shutdown() 导出接口
这是目前唯一跨平台、可测试、可审计的方案。核心是放弃“自动”,改为主动控制生命周期。
立即学习“C++免费学习笔记(深入)”;
- 声明为
static Singleton* instance = nullptr;,而非局部静态变量或智能指针 -
getInstance()内用std::call_once(init_flag, []{ instance = new Singleton(); });初始化,线程安全且无竞态 - 头文件中导出 C 风格函数:
extern "C" __attribute__((visibility("default"))) void Singleton_shutdown();(Linux)或extern "C" __declspec(dllexport) void Singleton_shutdown();(Windows) -
Singleton_shutdown()内只做三件事:if (instance) { delete instance; instance = nullptr; },不调用日志、不抛异常、不访问任何外部符号 - 宿主程序必须严格按顺序执行:
dlsym(handle, "Singleton_shutdown") → 调用 → dlclose(handle)(Windows 同理)
__attribute__((destructor)) 和 DllMain 只能作轻量兜底,不能主用
它们不是销毁主路径,而是“用户忘了调 shutdown 时的最后一道提醒”。一旦在里面写重逻辑,反而更容易崩溃。
- Linux 下
__attribute__((destructor)) static void on_unload() { if (instance) abort(); }—— 只用abort()或write(2)打印纯系统调用日志,禁用std::cout、malloc、printf - Windows 下
DllMain的DLL_PROCESS_DETACH分支中,必须判lpReserved == nullptr,仅在此条件下调用Singleton::shutdown(),且该函数内禁止虚函数调用、禁止 STL 容器操作 - 两者都不能执行
delete:因为此时 CRT 堆可能已失效,operator delete可能跳转到已卸载的 libc 符号 - 编译时加
-fvisibility=hidden会隐藏destructor函数,必须显式设visibility("default")
析构函数里只能做最原始的资源释放
哪怕你用了显式 shutdown(),`Singleton` 的析构函数仍可能在不可靠上下文中执行(比如信号处理、atexit 回调、主线程已退出)。所以它的内容必须极度克制。
- 只释放本类直接持有的原始资源:
if (fd != -1) close(fd);、if (handle) CloseHandle(handle);、if (ptr) free(ptr); - 禁止调用任何非本模块定义的函数:包括
LOG_INFO、Config::get()、db_pool->close() - 禁止使用
std::string、std::vector、std::mutex的成员函数 —— 它们的析构器可能依赖已失效的全局对象 - 线程相关操作必须先判空:
if (worker_thread.joinable()) worker_thread.join(); - 若需高层清理(如刷新日志缓冲区),必须提前在
shutdown()中完成,析构函数留空或仅置nullptr
真正危险的不是“怎么写单例”,而是“以为它会自动收尾”。只要动态库存在热加载/卸载需求,就必须把 new 和 delete 的时机、顺序、上下文全部显式暴露给宿主。其他所有技巧,都是在这个前提下补漏。


















