Meyer单例在动态库中必然失效——析构函数不执行,导致悬垂指针、未关闭句柄、未join线程,后续访问崩溃;根本原因是dlclose/FreeLibrary不保证触发局部静态变量析构,且跨模块调用会SEGFAULT。

动态库卸载时,static Singleton& getInstance() { static Singleton instance; } 这种 Meyer 单例**必然失效**——析构函数大概率不执行,留下悬垂指针、未关闭句柄、未 join 的线程,后续访问直接崩溃。这不是“可能出问题”,而是 Linux dlclose 和 Windows FreeLibrary 下的确定性行为。
为什么 Meyer 单例在 .so/.dll 里根本不能用
局部静态变量的析构由运行时统一管理,但 dlclose 不保证触发它;glibc 常跳过,尤其当 Singleton 析构中调用了日志、配置、甚至 std::cout 等跨模块符号时,地址空间已 unmapped,直接 SEGFAULT。更隐蔽的是:实例内存还在,但析构没跑 → std::thread 没 join()、FILE* 没 fclose()、自定义内存池没释放,其他模块若还存着该单例指针,解引用就是未定义行为。
-
static Singleton instance;:析构时机不可控,依赖 CRT 行为,禁用 -
static Singleton* instance = new Singleton();:堆内存永不释放,析构函数永不调用,同样危险 - 任何在析构函数里调用外部 API(日志、网络、STL iostream)的行为,卸载时必崩
Windows 下必须用 DllMain + DLL_PROCESS_DETACH 判空
这是唯一能精准响应 FreeLibrary 调用的机制,但关键在判空:lpReserved == nullptr 才表示是显式卸载,而非进程退出。此时才能安全调用销毁逻辑。
- 在
DllMain的DLL_PROCESS_DETACH分支内,先判断if (lpReserved == nullptr) - 仅在此条件下调用
Singleton::destroyInstance(),该函数需用std::atomic标记已销毁,防止重复 delete -
destroyInstance()内必须显式join()线程、close()文件描述符、munmap()内存映射,不能依赖成员变量析构 - 销毁后立即将静态指针置为
nullptr,并在getInstance()中检查,若为nullptr则抛std::runtime_error("Singleton destroyed") - 禁止在
DllMain中调用std::cout、malloc、跨模块函数或任何可能触发 DLL 加载的操作
Linux 下 __attribute__((destructor)) 只能作兜底,不能当主路径
该函数会在 dlclose 后执行,但执行环境极受限:其他共享库符号可能已不可见,malloc、std::string、甚至 std::mutex 析构都可能失效。它不能做实际清理,只能轻量确认。
- 定义
static void __attribute__((destructor)) so_cleanup() { if (Singleton::instance != nullptr) abort(); }—— 若非空,说明宿主忘了调用shutdown(),直接中止进程提醒 - 真正销毁必须由宿主程序在
dlclose()前显式调用导出的 C 函数,如extern "C" __attribute__((visibility("default"))) void Config_shutdown(); -
Config_shutdown()内部只做三件事:加锁、delete实例指针、置空、标记原子状态;禁止调用任何 STL 流、日志、第三方 SDK - 编译时确保未启用
-fvisibility=hidden,否则导出函数可能被剥离
显式 shutdown 接口是跨平台唯一可靠方案
把生命周期完全交还给宿主程序,是插件系统、热更新等场景的事实标准。它不依赖任何运行时析构保证,可控、可测、可调试。
- 头文件中导出两个 C 风格函数:
extern "C" void init_singleton();和extern "C" void destroy_singleton(); -
init_singleton()用std::call_once+new初始化裸指针,避免静态变量;destroy_singleton()仅执行delete和置空 - 宿主程序必须严格配对:
dlopen→init_singleton→ 使用 →destroy_singleton→dlclose;漏掉任意一步都会导致泄漏或二次析构 - 单例类的析构函数本身也必须极简:只释放本类直接持有的原始资源(如
open()返回的 fd、malloc()的内存),不碰任何外部对象 - 若使用 RAII 封装(如
DllLoader类),其析构函数必须检查并主动调用destroy_singleton函数指针,不能假设它已被调用
最易被忽略的一点:所有销毁路径(DllMain、destructor、显式 shutdown)都必须用同一套原子标记和空指针检查逻辑,否则多线程下仍可能 double-delete 或误判存活状态。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











