全局注册表线程安全需用std::mutex保护unordered_map的读写,禁止构造中注册、确保单例初始化安全、用weak_ptr存储服务引用、注册表仅作查找目录而非生命周期管理者。

全局注册表在多线程下出问题,90% 是因为没锁住读写、或在构造中提前注册、或生命周期没对齐。直接上 std::mutex 不一定错,但容易卡死、性能差、掩盖设计缺陷。
注册与查找必须用同一把锁保护容器访问
注册表本质是 std::unordered_map 这类非线程安全容器,任何并发的 insert、find、erase 都会触发未定义行为——崩溃、丢数据、返回脏值。不能靠“我只读不写”来侥幸。
- 用
std::mutex最稳妥:所有 public 接口(registerService、getService、unregister)开头加std::lock_guard<:mutex></:mutex> - 别用
std::shared_mutex除非你确认读远多于写且 profiler 显示锁争用是瓶颈;它在 Windows 上开销未必小,且 C++17 前部分标准库实现不完善 - 锁粒度别拆到 key 级:按前缀分桶加锁看似提升并发,但查不到时要遍历多个桶,逻辑复杂、易漏锁、debug 成本高
- 避免在锁内调用用户回调或长耗时操作(比如
Clone()或构造函数),否则会把整个注册表卡住
绝不在构造函数里调用 registerService
对象还没构造完,指针就进了全局表,其他线程一查就拿到半成品,dynamic_pointer_cast 可能失败,虚函数调用可能跳到未初始化的 vtable,UB 就来了。
- 工厂函数才是注册入口:比如
ServiceFactory::CreateAndRegister("db", ...),确保对象 fully constructed 后才进表 - 注册失败要回滚:如果
registerService抛异常,必须保证注册表状态不变(map 插入前先find检重,或用try_emplace) - 注册键名必须唯一且稳定:别用
this地址当 key,多线程下地址复用可能导致误覆盖
注册表单例的初始化时机必须线程安全
用 Meyer’s singleton(函数内 static 局部变量)是最简单可靠的方案。不要手写 double-checked locking,C++11 以后它既难写对又没必要。
-
static ServiceRegistry& instance() { static ServiceRegistry inst; return inst; }—— 初始化自动线程安全,且首次调用才构造 - 别在全局对象构造函数里访问
instance():静态对象初始化顺序不确定,可能 registry 还没建好就被调了 - 析构顺序风险:若其他静态对象的析构函数里还调用
getService,registry 可能已被销毁;建议显式调用dropAll()清空再让其自然析构 - 注册表本身别持有
shared_ptr到外部服务:否则可能延长服务生命周期,导致 shutdown 时资源无法释放
用 weak_ptr 替代裸指针或 shared_ptr 存储服务引用
注册表不是服务的所有者,只是个查找目录。用 std::shared_ptr<void></void> 存服务,等于强行绑定生命周期,极易引发循环引用或提前释放。
- 存
std::weak_ptr<void></void>:服务由业务方管理生命周期,注册表只负责“看到它还在”,查到后lock()得临时shared_ptr再用 -
getService返回std::shared_ptr<t></t>而不是T*:避免用户拿裸指针去缓存,导致悬挂 - 注册时检查
weak_ptr.expired():防止重复注册已销毁的服务实例 - 注意
lock()的开销:高频调用场景下,可考虑加一层本地缓存(带版本号或 timestamp),但需权衡一致性
最常被忽略的点是:注册表不该决定服务怎么活,只该决定怎么找。一旦开始在表里 new 对象、管理引用计数、甚至触发 reload,它就从“目录”退化成“管家”,线程安全边界立刻模糊——这时候该拆的是职责,不是加更多锁。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











