必须显式提供shutdown()函数并在dlclose/FreeLibrary前由宿主主动调用,配合裸指针、std::call_once、原子标志和分层清理,严禁在DllMain或destructor中执行delete或虚函数调用。

如果您在动态库中使用单例模式,但卸载时出现段错误、内存访问违例或资源泄漏,则很可能是单例对象在 dlclose() 或 FreeLibrary() 调用后仍被访问,或其析构函数在不可靠时机被触发。以下是针对动态库场景下确保单例安全注销的多种实战方案:
一、显式提供 shutdown() 函数并由宿主程序主动调用
该方法将销毁控制权完全交还给宿主程序,规避静态析构顺序不可控问题;shutdown() 必须幂等、线程安全,且仅执行本模块内可安全释放的操作。
1、在单例类中声明静态成员函数 static void shutdown(),内部使用 std::atomic
2、shutdown() 中先加锁(若单例支持并发访问),再执行 delete 实例指针,并将静态指针置为 nullptr。
3、在头文件中以 extern "C" 方式导出 C 风格函数,例如:extern "C" __declspec(dllexport) void Config_shutdown();(Windows)或 extern "C" __attribute__((visibility("default"))) void Config_shutdown();(Linux)。
4、宿主程序在调用 dlclose() 或 FreeLibrary() 前,必须通过 GetProcAddress() 或 dlsym() 获取该函数地址,并严格调用一次。
5、确保 shutdown() 内部不调用任何跨模块符号,包括日志、网络、STL iostream、std::mutex 析构等不可靠依赖。
二、在 DllMain 或 __attribute__((destructor)) 中触发幂等销毁
该方法利用操作系统提供的模块卸载钩子,在 DLL_PROCESS_DETACH 或 SO 卸载时自动介入;但需规避 CRT 堆失效、符号不可见等陷阱,仅适用于轻量清理。
1、Windows 下,在 DllMain 的 DLL_PROCESS_DETACH 分支中判断 lpReserved == nullptr(表示由 FreeLibrary 触发,而非进程退出),再调用单例的 destroyInstance()。
2、Linux 下,定义 __attribute__((destructor)) static void on_so_unload() { Singleton::destroyInstance(); },但必须确保该函数内不使用 STL 容器、不调用虚函数、不访问其他静态对象。
3、destroyInstance() 必须检查原子标志位,确认未销毁才执行 delete,并立即置空指针。
4、禁止在 destructor 函数中调用 std::cout、std::cerr、std::string 析构或任何依赖 C 运行时堆的代码。
5、若单例持有 std::thread 或 std::mutex,应在 destroyInstance() 中先 join 所有线程、unlock 所有互斥量,再释放内存。
三、采用裸指针 + std::atexit 注册 + 显式清理双保险机制
该方法结合进程级 atexit 与动态库级显式 shutdown,覆盖 dlclose 和进程退出两种路径;std::atexit 仅作兜底,不可替代主动 shutdown。
1、单例 getInstance() 中首次构造后,立即调用 std::atexit(&cleanup_at_exit) 注册清理函数,但明确注释“仅对进程退出有效”。
2、cleanup_at_exit 函数内检查原子标志位,若尚未销毁则调用核心销毁逻辑,并标记已处理。
3、同时保留 shutdown() 函数供 dlclose 前调用,并在 shutdown() 中调用同一套核心销毁逻辑。
4、核心销毁逻辑封装为私有静态函数,例如 do_destroy(),统一执行 delete、置空、关闭文件句柄、unmap 共享内存等本模块可控操作。
5、所有对外接口(getInstance、shutdown、atexit 回调)均读写同一 std::atomic
四、使用 RAII 封装的 DllLoader 管理生命周期
该方法将动态库加载与单例注销绑定于同一作用域,避免宿主程序遗漏 shutdown 调用;适用于 C++ 宿主且能控制加载流程的场景。
1、定义 DllLoader 类,构造时调用 LoadLibrary/dlopen,析构时先调用 cleanup_func(通过 GetProcAddress/dlsym 获取),再调用 FreeLibrary/dlclose。
2、在 DllLoader 构造函数中,通过 GetProcAddress 获取 shutdown 函数指针,并保存为成员变量。
3、在 DllLoader 析构函数中,首先检查 cleanup_func 是否非空,若存在则调用它;然后才执行 FreeLibrary/hModule 释放。
4、确保 DllLoader 实例生命周期严格长于任何对该动态库单例的访问——即单例使用必须在其作用域内完成。
5、禁止在 DllLoader 析构函数中捕获异常或执行复杂逻辑,仅做函数指针判空与调用,保持轻量与确定性。
五、禁用 thread_local 缓存并改用惰性初始化规避卸载死锁
当单例内部使用 thread_local 存储线程私有资源时,其析构可能晚于单例本身,导致析构函数访问已销毁成员;必须切断该依赖链。
1、删除所有形如 thread_local std::unique_ptr
2、改用函数内静态局部变量实现惰性初始化,例如:static auto& per_thread = []{ return std::make_unique
3、若线程局部对象需感知单例销毁状态,在单例 shutdown() 中设置全局原子标志 std::atomic
4、禁止在 thread_local 变量的析构函数中调用单例的 getInstance()、任何成员函数或静态方法。
5、对 thread_local 持有的资源(如 socket、file descriptor),应在单例 shutdown() 后、线程退出前由业务层显式释放,不依赖自动析构。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











