在企业开发中经常需要用到观察者模式:一些对象(Observer)作为观察者,对目标对象(Subject)的某些变化感兴趣,当变化发生时,Observer 需要及时获得通知,以便迅速做出改变或执行某些操作。
如果把这套“订阅—通知”的逻辑直接散落在业务代码里,很快就会遇到三个麻烦:订阅列表由谁维护、观察者销毁后留下的悬垂指针怎么处理、回调执行过程中又去注销自己导致死锁怎么办。本文实现一个通用的线程安全订阅通知框架 Notifier,并给出一个可编译运行的完整示例,说明如何在业务代码中低成本复用它。
一、设计目标 动手前先明确四条约束,后文的每个设计细节都是为它们服务的:
线程安全 :注册、注销、广播可能发生在任意线程,所有共享数据都要用互斥锁保护。
生命周期安全 :事件中心不“拥有”观察者。观察者销毁后自动脱离,既没有悬垂指针,也不会因为事件中心持有 shared_ptr 而导致对象永远无法释放。
低侵入 :被观察者只需继承模板并在事件发生时调用一次广播。观察者只需实现自己的回调接口,不依赖被观察者的具体类型。
可复用 :失效清理、加锁快照、参数转发这些容易写错的“脏活”只写一遍。
二、核心实现:Notifier 模板 完整的头文件实现如下,后面再逐点拆解关键设计。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 #pragma once #include <algorithm> #include <functional> #include <map> #include <mutex> #include <string> #include <vector> #include <memory> template <typename CallbackT>class Notifier { public : using Callback = CallbackT; using CallbackWeakPtr = std::weak_ptr<CallbackT>; using CallbackPtr = std::shared_ptr<CallbackT>; using CallbackList = std::vector<CallbackWeakPtr>; virtual ~Notifier () = default ; void registerCallback (CallbackPtr callback) { if (callback == nullptr ) return ; std::lock_guard<std::mutex> lock (mDataLock) ; if (std::find_if (mCallbacks.begin (), mCallbacks.end (), [callback](CallbackWeakPtr mWeakCallback) { if (auto mCallback = mWeakCallback.lock ()) return callback == mCallback; return false ; }) == mCallbacks.end ()) { mCallbacks.push_back (callback); } } void unRegisterCallback (CallbackPtr callback) { if (callback == nullptr ) return ; std::lock_guard<std::mutex> lock (mDataLock) ; auto iter = std::find_if (mCallbacks.begin (), mCallbacks.end (), [callback](CallbackWeakPtr mWeakCallback) { if (auto mCallback = mWeakCallback.lock ()) { return callback == mCallback; } return false ; }); if (iter != mCallbacks.end ()) { mCallbacks.erase (iter); } } size_t getRegisteredCallbacksCount () { std::lock_guard<std::mutex> lock (mDataLock) ; return mCallbacks.size (); } protected : CallbackList mCallbacks; std::mutex mDataLock; using Closure = std::function<void (std::shared_ptr<Callback>)>; void potentiallyFireNotification (Closure closure, std::function<bool (CallbackWeakPtr)> shouldFire = [](CallbackWeakPtr) -> bool { return true ; }) { CallbackList liveCallbacks = removeExpiredCallbacks (); for (auto callback : liveCallbacks) { if (auto lockedCallback = callback.lock ()) { if (shouldFire (lockedCallback)) { closure (lockedCallback); } } } } template <typename T> const T& to_const_ref_or_copy (T& arg) { return arg; } template <typename T> T to_const_ref_or_copy (T&& arg) { return std::remove_reference_t <T>{ arg }; } template <typename Func, typename ... Args> void fireNotification (Func func, Args&&... args) { const auto liveCallbacks = removeExpiredCallbacks (); const auto enablePerfectForwarding = liveCallbacks.size () == 1 ; for (const auto & callback : liveCallbacks) { if (auto lockedCallback = callback.lock ()) { if (enablePerfectForwarding) { std::invoke (func, lockedCallback, std::forward<Args>(args)...); } else { std::invoke (func, lockedCallback, to_const_ref_or_copy (std::forward<Args>(args))...); } } } } virtual void callbackErased (CallbackWeakPtr) { } private : CallbackList removeExpiredCallbacks () { CallbackList liveCallbacks; { std::lock_guard<std::mutex> lock (mDataLock) ; if (mCallbacks.empty ()) { return {}; } liveCallbacks.reserve (mCallbacks.size ()); for (auto callbackIter = mCallbacks.begin (); callbackIter != mCallbacks.end (); ++callbackIter) { auto callback = *callbackIter; if (!callback.expired ()) { liveCallbacks.push_back (callback); } else { callbackErased (callback); } } if (mCallbacks.size () != liveCallbacks.size ()) { mCallbacks = liveCallbacks; } } return liveCallbacks; } };
接口一览
成员
访问级别
作用
registerCallback()
public
注册观察者(传入 shared_ptr),内部自动去重
unRegisterCallback()
public
主动注销观察者
getRegisteredCallbacksCount()
public
当前注册数量,便于调试与测试
fireNotification()
protected
向所有存活观察者广播事件
potentiallyFireNotification()
protected
带 shouldFire 谓词的条件广播
callbackErased()
protected virtual
观察者失效时的清理钩子,供子类同步旁路数据
removeExpiredCallbacks()
private
清理失效 weak_ptr 并返回存活快照
三个关键设计 1. 只保存 weak_ptr,事件中心不拥有观察者。 注册时接收 shared_ptr,存储时转成 weak_ptr。观察者的生死由其创建者决定,事件中心只是一个“通讯录”。通讯录里某一项失效了,广播时顺手划掉即可,不会延长任何对象的生命周期。
2. 先加锁快照,再解锁广播。 removeExpiredCallbacks() 在锁内完成三件事:拷贝存活项、清理失效项、回写容器。锁释放之后才开始遍历快照执行回调。这样即使回调内部又去注册、注销自己或他人,也不会出现 std::mutex 重入死锁,更不会踩到迭代器失效的未定义行为。
3. 参数转发按观察者数量切换策略。 只有一个观察者时可以放心完美转发,支持 move-only 参数、零拷贝。有多个观察者时退化为 to_const_ref_or_copy——左值传 const 引用、右值拷贝一份,避免第一个回调把右值“消费”掉之后,后续观察者拿到被掏空的数据。
扩展点:条件广播与清理钩子 potentiallyFireNotification() 把“通知谁”的过滤逻辑以 shouldFire 谓词的形式下放给子类,而快照、清理、加锁、lock() 保活这些共性逻辑仍由基类统一负责。callbackErased() 则给维护了旁路数据结构的子类(例如按 key 建索引的通知器)留了一个同步清理的时机,调用时锁已被持有,实现中不要再加锁。
三、使用示例:一匹马 假设有一匹马(被观察者),我们对它的“跳”“吃”“睡”三个动作感兴趣。观察者实现回调接口 IHorseCallback,在这三类事件发生时执行打印动作。
3.1 回调接口与被观察者 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #pragma once #include <iostream> #include <string> #include "Notifier.h" class IHorseCallback { public : virtual ~IHorseCallback () = default ; virtual void onJump (int heightCm) = 0 ; virtual void onEat (const std::string& food) = 0 ; virtual void onSleep (int minutes) = 0 ; virtual bool careAboutRace () const { return false ; } virtual void onRaceStart () {} }; class Horse : public Notifier<IHorseCallback>{ public : void jump (int heightCm) { std::cout << "[Horse] jumped " << heightCm << " cm" << std::endl; fireNotification (&IHorseCallback::onJump, heightCm); } void eat (const std::string& food) { std::cout << "[Horse] eating " << food << std::endl; fireNotification (&IHorseCallback::onEat, food); } void sleep (int minutes) { std::cout << "[Horse] sleeping " << minutes << " min" << std::endl; fireNotification (&IHorseCallback::onSleep, minutes); } void raceStart () { potentiallyFireNotification ( [](CallbackPtr observer) { observer->onRaceStart (); }, [](CallbackWeakPtr weak) { auto observer = weak.lock (); return observer != nullptr && observer->careAboutRace (); }); } };
这里的 fireNotification(&IHorseCallback::onJump, heightCm) 值得单独说明:第一个参数是回调接口的成员函数指针,它与 std::shared_ptr<IHorseCallback> 一起构成 std::invoke 的调用对象,因此子类只需要描述“发生了什么”,不必自己写遍历循环。
3.2 观察者 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #pragma once #include <iostream> #include <string> #include <utility> #include "Horse.h" class HorseWatcher : public IHorseCallback{ public : explicit HorseWatcher (std::string name) : mName(std::move(name)) { } void onJump (int heightCm) override { std::cout << " [" << mName << "] 打印:马跳了,高度 " << heightCm << " cm" << std::endl; } void onEat (const std::string& food) override { std::cout << " [" << mName << "] 打印:马吃了 " << food << std::endl; } void onSleep (int minutes) override { std::cout << " [" << mName << "] 打印:马睡了 " << minutes << " 分钟" << std::endl; } private : std::string mName; };
3.3 组装与运行 源文件保存为 UTF-8,用 g++ -std=c++17 main.cpp -o demo 即可编译运行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #include <iostream> #include <memory> #include "Horse.h" #include "HorseWatcher.h" int main () { Horse horse; auto watcher1 = std::make_shared <HorseWatcher>("Watcher-1" ); auto watcher2 = std::make_shared <HorseWatcher>("Watcher-2" ); horse.registerCallback(watcher1); horse.registerCallback(watcher2); horse.registerCallback(watcher2); std::cout << "registered = " << horse.getRegisteredCallbacksCount () << std::endl << std::endl; horse.jump (120 ); horse.eat ("apple" ); horse.sleep (30 ); horse.unRegisterCallback (watcher2); std::cout << std::endl << "after unregister watcher2" << std::endl; horse.jump (60 ); watcher1.reset (); std::cout << std::endl << "after watcher1 destroyed" << std::endl; horse.jump (30 ); std::cout << "registered = " << horse.getRegisteredCallbacksCount () << std::endl; return 0 ; }
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 registered = 2 [Horse] jumped 120 cm [Watcher-1] 打印:马跳了,高度 120 cm [Watcher-2] 打印:马跳了,高度 120 cm [Horse] eating apple [Watcher-1] 打印:马吃了 apple [Watcher-2] 打印:马吃了 apple [Horse] sleeping 30 min [Watcher-1] 打印:马睡了 30 分钟 [Watcher-2] 打印:马睡了 30 分钟 after unregister watcher2 [Horse] jumped 60 cm [Watcher-1] 打印:马跳了,高度 60 cm after watcher1 destroyed [Horse] jumped 30 cm registered = 0
最后一行的 registered = 0 值得注意:watcher1.reset() 之后,mCallbacks 里那个 weak_ptr 已经失效,但它是在下一次广播(removeExpiredCallbacks)时才被顺手清理掉的,Horse 完全不需要感知观察者的生命周期。
3.4 示例要点
被观察者极薄 :Horse 里没有锁、没有容器、没有生命周期管理,新增事件就是新增一个成员函数。
接口隔离 :观察者只依赖 IHorseCallback,既不需要继承 Notifier,也不需要知道 Horse 的存在,可以自由复用、单独测试。
所有权清晰 :Horse 不拥有观察者。观察者无论是 UI 面板、缓存对象还是临时对象都无所谓,谁创建谁持有,销毁即自动脱离。
线程安全 :多个线程同时调用 jump()/eat()/sleep() 与 registerCallback()/unRegisterCallback() 都是安全的。回调执行期间不持锁,回调内部再注册或注销也不会死锁。
四、进阶:只通知部分观察者 上面的 raceStart() 用到了基类的 potentiallyFireNotification():在 Horse 一侧声明“这次通知只发给关心比赛的观察者”,过滤谓词由子类提供,而快照、清理失效项、加锁、逐个 lock() 这些容易出错的活依然由基类统一完成。例如再写一个只关心比赛、不关心吃喝睡的观察者:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class RaceCommentator : public IHorseCallback{ public : void onJump (int ) override {} void onEat (const std::string&) override {} void onSleep (int ) override {} bool careAboutRace () const override { return true ; } void onRaceStart () override { std::cout << " [Commentator] 打印:比赛开始,马已就位" << std::endl; } };
把它和 HorseWatcher 一起注册后调用 horse.raceStart(),只有 RaceCommentator 会打印。同一个事件中心,既支持“广播给所有人”,也支持“广播给某一类人”。
五、小结 Notifier 把订阅通知中最容易出错的部分——生命周期、并发、参数语义——一次性封装干净,业务侧只剩下“表达事件”和“处理事件”两件事。落地时记住三条约定即可:
观察者一律用 shared_ptr 持有,注册后不必手动注销,销毁即自动脱离。
广播只在事件发生的线程上同步执行,回调里不要做长时间的阻塞操作。确需耗时处理时,转投到自己的线程池。
需要按类型、按兴趣定向通知时,优先扩展 shouldFire 谓词,而不是在子类里直接操作 mCallbacks。