std::call_once + std::once_flag 是确保多线程下动态库加载与符号解析仅执行一次的唯一安全方案,避免dlopen重复调用导致的符号混乱、全局构造函数多次执行及dlerror不可靠等问题。

std::call_once + std::once_flag 是唯一安全的初始化方案
多个线程同时调用 dlopen 加载同一个动态库(如 "libmath.so"),不是“可能出错”,而是明确违反 POSIX 规范:dlopen 本身不保证线程安全,重复加载同一路径的库可能导致符号表混乱、全局构造函数多次执行,甚至 dlerror() 返回不可靠。直接加互斥锁保护整个 dlopen 调用链也治标不治本——锁粒度太粗,且无法防止其他线程在你 unlock 后立刻重复加载。
正确做法是用 std::call_once 确保「加载 + 符号解析」这一整套操作仅执行一次:
- 声明一个全局或静态的
std::once_flag变量(必须是 static 或 namespace 作用域) - 把
dlopen、dlsym、错误检查全部包进 lambda 或函数里,传给std::call_once - 后续所有线程都通过已缓存的函数指针(如
cosine)调用,不再触碰 dlopen
static void* g_math_handle = nullptr;
static double (*g_cosine)(double) = nullptr;
static std::once_flag g_math_init_flag;
<p>void init_math_lib() {
g_math_handle = dlopen("./libmath.so", RTLD_LAZY | RTLD_GLOBAL);
if (!g_math_handle) {
// 处理 dlerror()
return;
}
g_cosine = reinterpret_cast<double>(dlsym(g_math_handle, "cos"));
}</double></p><p>// 所有线程首次需要 cos 时统一走这里
double safe_cos(double x) {
std::call_once(g_math_init_flag, init_math_lib);
return g_cosine ? g_cosine(x) : 0.0;
}
</p>
RTLD_NOLOAD + dlsym 组合可避免重复加载开销
如果库已被其他模块(比如主程序或另一个线程)隐式加载过,再调用 dlopen("./libmath.so", RTLD_LAZY) 仍会触发完整加载流程,浪费时间且可能引发引用计数异常。此时应优先尝试 RTLD_NOLOAD 模式——它只查找已加载的库,不执行映射和重定位。
典型场景:主程序启动时已链接 -lmath,但某个插件模块仍手动 dlopen 同名库。这时应:
- 先用
dlopen("./libmath.so", RTLD_NOLOAD)尝试获取句柄 - 若返回
nullptr,再 fallback 到RTLD_LAZY完整加载 - 始终搭配
RTLD_GLOBAL(尤其当后续模块需访问该库符号时)
注意:RTLD_NOLOAD 在 macOS 上对应 RTLD_FIRST 行为,Linux 下才真正“不加载”。跨平台项目需条件编译。
dlclose 不应被多线程随意调用
看似“谁加载谁关闭”很合理,但 dlclose 实际做的是引用计数减一,**只有当计数归零时才真正卸载**。多线程环境下,若线程 A 调用 dlclose 导致计数变 0,而线程 B 正在执行该库中某个函数,就会触发 SIGSEGV——因为代码段已被 unmmap。
因此生产环境应遵循:
- 禁止在工作线程中调用
dlclose;所有dlclose必须由主线程或明确的资源管理器在程序退出前统一执行 - 若必须支持热插拔,改用引用计数智能封装(例如
std::shared_ptr管理 handle,自定义 deleter 调用dlclose),并确保所有线程完成调用后再释放 - Linux 下可检查
/proc/self/maps验证库是否仍在内存,但这是调试手段,不可用于逻辑判断
Windows 下 LoadLibrary/GetProcAddress 的并发等价写法
Windows 没有 std::call_once 的原生替代,但 LoadLibrary 本身对同一路径的多次调用是线程安全的——它内部维护引用计数并返回相同 HMODULE。所以 Windows 场景下重点不是防重复加载,而是防重复 GetProcAddress 和资源泄漏:
- 仍需用
std::call_once保护GetProcAddress获取函数指针的过程(因为该操作无原子性) -
LoadLibrary返回非 NULL 后,可直接缓存 HMODULE,不必每次调用 - 绝对避免在 DLL_PROCESS_DETACH 中调用
FreeLibrary—— 这会导致递归卸载崩溃
关键差异:Linux 的 dlopen 默认不线程安全,Windows 的 LoadLibrary 默认安全;但两者都要求符号解析(dlsym/GetProcAddress)必须同步保护。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











