meyer单例在动态库中必然失效:dlclose/freelibrary大概率跳过析构,导致悬垂指针、资源泄漏和segfault;必须改用堆分配+std::call_once初始化+显式shutdown销毁,由宿主程序严格控制生命周期。

动态库卸载时,static Singleton& getInstance() { static Singleton instance; } 这种 Meyer 单例**一定不可靠**——不是“可能崩溃”,而是 dlclose 和 FreeLibrary 下大概率跳过析构,留下悬垂指针、未关闭句柄、未 join 线程,后续任意访问即段错误。
为什么 Meyer 单例在 .so/.dll 里必然失效
局部静态变量的析构由运行时统一调度,但动态库卸载不触发该调度:glibc 在多数 dlclose 场景下直接跳过静态局部变量析构;Windows 的 CRT 也不保证 DLL_PROCESS_DETACH 中执行 C++ 静态对象析构。更危险的是:
- 析构函数若调用
std::cout、日志宏、Config::getInstance(),这些符号可能已从地址空间卸载,直接SEGFAULT -
instance所在堆内存没被回收,但析构函数没跑 →std::thread仍在运行、int fd没close()、内存池没释放 - 其他模块持有该单例裸指针(如插件传入的
Logger*),卸载后解引用就是未定义行为
必须改用堆分配 + std::call_once + 显式 shutdown()
放弃“自动析构”幻想,把生命周期控制权拿回来。核心是三件事:加载时强制构造、运行时安全访问、卸载前主动销毁。
- 声明为
static Singleton* instance = nullptr;,而非局部静态对象或全局静态对象 -
getInstance()内用std::call_once(flag, []{ instance = new Singleton(); });保证线程安全初始化 - 提供导出函数
extern "C" __attribute__((visibility("default"))) void Config_shutdown();(Linux)或extern "C" __declspec(dllexport) void Config_shutdown();(Windows) -
shutdown()内部先检查std::atomic<bool> s_destroyed</bool>,幂等;然后delete instance;,再置instance = nullptr; -
shutdown()中禁止调用任何外部 API:不碰std::cout、不调日志、不访问其他单例、不 new/delete、不抛异常
Windows 必须在 DllMain 的 DLL_PROCESS_DETACH 中判空 lpReserved
这是 Windows 平台最可控的卸载钩子,但关键在区分卸载来源:进程退出时不能清理,否则会访问已销毁的 CRT 或堆。
- 仅当
lpReserved == nullptr时,才表示本次卸载由FreeLibrary主动触发,此时可调用Singleton::destroyInstance() -
destroyInstance()必须和shutdown()逻辑一致:原子判空、加锁(如有并发访问)、delete、置空指针 - 禁止在
DllMain中调用std::mutex::lock()、std::thread::join()等可能引发 DLL 加载/卸载依赖的函数;资源清理必须提前在destroyInstance()中完成 -
getInstance()返回前应检查instance != nullptr,否则抛std::runtime_error("Singleton destroyed")
Linux 的 __attribute__((destructor)) 只能作兜底,不能当主力
它会在 dlclose 后执行,但执行环境极度受限:STL、malloc、甚至 libc 符号都可能已不可用。它不是清理入口,而是“提醒你漏了事”的哨兵。
- 定义
static void __attribute__((destructor)) so_cleanup() { if (instance != nullptr) abort(); }—— 不尝试delete,只做致命提醒 - 若真要执行清理,必须确保
destroyInstance()内部只操作原始资源:if (fd != -1) close(fd);、if (handle) CloseHandle(handle);、if (ptr) free(ptr); - 编译时禁用
-fvisibility=hidden,否则destructor函数可能被剥离 - 真正可靠的清理路径永远是宿主程序显式调用
dlsym(handle, "Config_shutdown"),再dlclose(handle)
最易被忽略的一点:所有跨模块依赖(比如单例内部调用了 NetworkManager::getInstance())必须在 shutdown() 中**提前断开**,不能等到析构函数里去访问——因为那个时刻,NetworkManager 所在的库可能已经卸载了。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











