公司动态

C++11观察者模式:现代实现、线程安全与性能优化

📅 2026/8/28 6:49:11
C++11观察者模式:现代实现、线程安全与性能优化
1. 项目概述为什么我们需要在C11时代重新审视观察者模式在软件开发的日常里我们经常遇到一个场景一个对象我们称之为“主题”的状态发生了变化而其他一系列对象我们称之为“观察者”需要立刻知道这个变化并做出相应的反应。比如一个图形用户界面GUI中的按钮被点击了菜单、工具栏、状态栏都需要更新又或者一个游戏引擎中的角色生命值发生了变化血条UI、音效系统、任务系统都需要被通知。这种“一对多”的依赖关系如果直接用硬编码的方式去调用代码会变得高度耦合难以维护和扩展。这时候设计模式就派上用场了而观察者模式Observer Pattern正是为解决这类问题而生的经典模式。那么为什么还要专门用C11来实现它呢这不仅仅是“用新语法重写旧模式”那么简单。C11标准为这门语言带来了革命性的变化引入了智能指针、lambda表达式、移动语义、右值引用、std::function和std::bind等一系列现代特性。这些特性让我们能够以更安全、更高效、更优雅的方式来实现观察者模式彻底告别过去那些基于原始指针、手动管理内存、接口臃肿的实现方式。使用C11我们可以轻松解决内存泄漏、悬垂指针的问题让回调的注册和使用变得异常灵活代码的可读性和可维护性也大大提升。这篇文章就是从一个常年奋战在一线的C开发者的角度来和你一起拆解如何用C11的特性打造一个工业级强度的观察者模式实现。我们会从最基础的思路开始一步步深入到线程安全、性能优化等高级话题并分享我在实际项目中踩过的坑和总结的经验。无论你是刚接触设计模式的初学者还是想优化现有代码的老手相信都能从中获得一些实用的启发。2. 核心设计思路从传统模式到现代C的演进在动手写代码之前我们先得把思路理清楚。观察者模式的核心思想是“解耦”让主题和观察者之间不直接依赖而是通过一个抽象的接口进行通信。传统C98/03时代的实现大致框架是这样的定义一个抽象的Observer观察者接口通常包含一个update()之类的纯虚函数。具体的观察者类继承这个接口实现自己的update逻辑。定义一个Subject主题基类或具体类内部维护一个观察者指针通常是原始指针的列表。Subject提供attach注册、detach注销和notify通知方法。当主题状态变化时调用notify遍历列表调用每个观察者的update方法。这个框架本身没问题但问题出在实现细节上尤其是在C中。使用原始指针管理观察者列表谁负责释放这些指针观察者先于主题被销毁怎么办这就是典型的资源管理和对象生命周期问题。C11的智能指针特别是std::shared_ptr和std::weak_ptr为我们提供了完美的解决方案。我的设计思路是进行一场“现代化改造”用std::shared_ptr管理观察者对象的所有权主题持有观察者的std::shared_ptr意味着只要主题还存在它注册的观察者就不会被意外释放。这解决了手动管理内存的麻烦。但更要警惕循环引用如果观察者也反向持有主题的shared_ptr就会形成循环引用导致内存永远无法释放。因此在主题内部我们应使用std::weak_ptr来引用观察者。weak_ptr是一种“弱引用”它不会增加对象的引用计数因此不会阻止其所指对象被销毁。在通知时我们再尝试将weak_ptr提升lock()为shared_ptr如果提升成功说明观察者还活着就调用它如果失败说明观察者已被销毁我们就安全地将其从列表中移除。这是现代C实现观察者模式最核心、最安全的技巧。用std::function替代固定的接口为什么观察者一定要继承自某个基类、实现某个特定签名的update函数呢C11的std::function提供了通用的可调用对象包装器。我们可以让主题接受任何可调用对象函数、lambda、仿函数、绑定后的成员函数等作为观察者。这极大地增加了灵活性实现了彻底的接口解耦。利用std::mutex保证线程安全在现代多线程程序中主题和观察者很可能在不同的线程中被访问和修改。对观察者列表的增删改查操作必须是原子的否则会导致数据竞争和未定义行为。我们需要用互斥锁来保护这个共享资源。基于以上思路我们的现代观察者模式将围绕以下几个核心组件构建一个使用std::vectorstd::weak_ptr...或类似结构存储观察者的主题类一个用于包装任意回调的std::function以及确保线程安全的锁机制。2.1 为何选择weak_ptrfunction的组合这是一个关键的设计决策。让我们深入分析一下为什么这是最佳组合。使用weak_ptr的必要性想象一个场景一个UI组件观察者订阅了某个数据模型主题的变化。当用户关闭这个UI窗口时组件对象被销毁。如果主题仍然持有该组件的shared_ptr那么这个组件对象将因为引用计数不为零而无法被正确释放导致内存泄漏。如果主题持有的是weak_ptr则不会影响组件的生命周期。当主题下次通知时通过lock()会发现该weak_ptr已失效从而可以安全地清理这个“僵尸”观察者。这实现了观察者生命周期的自动管理是资源安全的基石。使用std::function的灵活性传统的基于继承的接口方式强制所有观察者必须拥有相同的函数签名如void update(int)。这很不灵活。也许有的观察者只需要一个事件通知不需要参数有的需要丰富的上下文信息。使用std::function我们可以定义主题通知时传递的参数比如一个包含事件详情的结构体而观察者只需要提供一个能接受该参数的函数即可。它可以是全局函数、类的静态成员函数、通过std::bind绑定了对象的成员函数或者一个捕获了上下文的lambda表达式。这种灵活性让代码的适应性变得极强。// 传统方式必须继承 class MyObserver : public Observer { public: void update(int value) override { /* ... */ } }; // 现代方式任何可调用对象都可以 subject.attach([](const Event e) { std::cout “Lambda caught: ” e.id std::endl; }); subject.attach(std::bind(MyClass::onEvent, myObj, std::placeholders::_1));这种组合带来的好处是安全与灵活并存。既避免了内存问题又解耦了接口约束这正是现代C设计所追求的目标。3. 核心实现细节与类设计接下来我们进入具体的实现环节。我将展示一个支持模板化事件类型、线程安全、且易于使用的观察者模式实现。3.1 定义事件类型与观察者别名首先我们不固定事件类型而是使用模板让主题可以通知任何类型的事件数据。同时我们定义观察者的类型为一个接受特定事件类型的std::function。#include memory #include functional #include vector #include mutex #include algorithm // 前向声明主题类 template typename EventT class Subject; // 观察者类型一个接收EventT类型参数的函数对象 template typename EventT using Observer std::functionvoid(const EventT); // 观察者弱引用类型 template typename EventT using ObserverWeakPtr std::weak_ptrObserverEventT; // 观察者强引用类型主要用于外部保存 template typename EventT using ObserverPtr std::shared_ptrObserverEventT;这里的关键点是Observer本身是一个std::function而我们将它的shared_ptr和weak_ptr进行了别名定义。为什么需要shared_ptrObserver因为std::function本身是可拷贝的类型但有时我们希望能够明确地标识和注销某个特定的观察者回调。将其包装进shared_ptr我们就得到了一个唯一的、可管理的句柄。3.2 实现主题Subject类主题类是整个模式的核心。它需要管理一个观察者列表并提供注册、注销和通知的方法。template typename EventT class Subject { public: Subject() default; ~Subject() default; // 禁止拷贝和赋值通常主题是唯一的。如果需要可以手动实现或启用移动语义。 Subject(const Subject) delete; Subject operator(const Subject) delete; /** * 注册一个观察者。 * param observer 观察者函数对象 * return 返回一个ObserverPtr可用于后续显式注销该观察者。 */ ObserverPtrEventT attach(ObserverEventT observer) { auto observerPtr std::make_sharedObserverEventT(std::move(observer)); { std::lock_guardstd::mutex lock(mutex_); observers_.emplace_back(observerPtr); } return observerPtr; } /** * 注销一个观察者通过weak_ptr。 * 这是线程安全的惰性删除。实际删除发生在notify时。 * param observerWeak 要注销的观察者的弱引用 */ void detach(const ObserverWeakPtrEventT observerWeak) { std::lock_guardstd::mutex lock(mutex_); // 我们只是标记一下真正的清理在notify时进行。 // 这里可以将对应的weak_ptr重置或者放入一个待删除列表。 // 一种简单实现在notify遍历时跳过无法lock的weak_ptr并移除。 // 另一种做法这里直接查找并移除。我们采用后者更及时。 auto it std::find_if(observers_.begin(), observers_.end(), [observerWeak](const ObserverWeakPtrEventT wp) { return !(wp.owner_before(observerWeak) || observerWeak.owner_before(wp)); }); if (it ! observers_.end()) { observers_.erase(it); } } /** * 通知所有观察者。 * param event 要传递的事件对象 */ void notify(const EventT event) { std::vectorObserverPtrEventT validObservers; { std::lock_guardstd::mutex lock(mutex_); // 1. 清理失效的观察者 observers_.erase( std::remove_if(observers_.begin(), observers_.end(), [](const ObserverWeakPtrEventT wp) { return wp.expired(); }), observers_.end()); // 2. 收集当前有效的观察者强引用避免在调用回调时持有锁。 validObservers.reserve(observers_.size()); for (const auto weakObserver : observers_) { if (auto strongObserver weakObserver.lock()) { validObservers.push_back(strongObserver); } } } // 锁在这里释放 // 3. 在无锁状态下调用观察者 for (const auto observer : validObservers) { try { (*observer)(event); // 调用std::function } catch (...) { // 强烈建议单个观察者的异常不应影响其他观察者。 // 这里可以记录日志但继续执行。 // 在实际项目中需要定义更完善的错误处理策略。 } } } // 获取当前观察者数量主要用于调试 size_t observerCount() const { std::lock_guardstd::mutex lock(mutex_); return observers_.size(); } private: mutable std::mutex mutex_; std::vectorObserverWeakPtrEventT observers_; };这个实现包含了几个重要的设计点和技巧线程安全所有对observers_容器的修改操作attach,detach,notify中的清理和收集都通过std::lock_guard保护。notify方法中我们先收集有效的观察者强引用到一个局部向量然后释放锁最后再调用回调。这是关键优化如果在持有锁的情况下调用用户提供的回调函数万一回调函数执行时间很长或者它内部又尝试去attach/detach同一个主题造成递归锁或死锁就会导致性能瓶颈甚至死锁。先收集再调用的方式避免了这个问题。惰性清理与及时清理结合在notify中我们先使用std::remove_if和expired()方法清理掉所有已经失效的weak_ptr。detach方法也提供了主动移除的途径。两种方式结合保证了列表的整洁。异常安全在遍历调用观察者时我们用try-catch块包裹了每个调用。确保一个观察者的崩溃抛出异常不会阻止其他观察者接收到通知。在生产环境中这里应该记录下异常信息以便调试。使用std::move优化在attach中我们使用std::move(observer)来转移传入的std::function避免不必要的拷贝。注意weak_ptr的比较不能直接用。我们使用了owner_before来检查两个weak_ptr是否指向同一个控制块这是标准库推荐的方式来判断weak_ptr是否“等价”。3.3 如何使用这个现代观察者模式下面我们通过一个简单的例子来演示如何使用上面实现的Subject类。#include iostream #include string // 定义一个具体的事件类型 struct ButtonClickEvent { int buttonId; std::string buttonName; long timestamp; }; int main() { SubjectButtonClickEvent buttonSubject; // 观察者1使用Lambda表达式 auto observer1 buttonSubject.attach([](const ButtonClickEvent e) { std::cout “[Lambda] Button clicked: ” e.buttonName “ (ID: ” e.buttonId “)” std::endl; }); // 观察者2使用普通函数 void logEvent(const ButtonClickEvent e); auto observer2 buttonSubject.attach(logEvent); // 观察者3使用绑定成员函数 class Logger { public: void onButtonClicked(const ButtonClickEvent e) { std::cout “[Logger] Click recorded at ” e.timestamp std::endl; } }; Logger myLogger; auto observer3 buttonSubject.attach(std::bind(Logger::onButtonClicked, myLogger, std::placeholders::_1)); // 模拟事件发生 ButtonClickEvent event{1001, “SubmitButton”, 1234567890}; std::cout “Notifying observers...“ std::endl; buttonSubject.notify(event); std::cout “Current observer count: ” buttonSubject.observerCount() std::endl; // 注销一个观察者 std::cout “\nDetaching observer1...” std::endl; buttonSubject.detach(observer1); buttonSubject.notify(event); // 这次observer1不会被调用 std::cout “Current observer count after detach: ” buttonSubject.observerCount() std::endl; // observer2和observer3会在main函数结束时随着buttonSubject的销毁 // 其weak_ptr在notify时被清理不会造成内存泄漏。 return 0; } void logEvent(const ButtonClickEvent e) { std::cout “[Function] Click event logged.” std::endl; }这个例子展示了现代实现的巨大优势注册观察者变得极其自由。你不再需要为了一个回调而去继承一个基类并实现虚函数任何可调用对象都可以直接“扔”给主题。代码简洁意图清晰。4. 高级话题与性能优化一个基础的、线程安全的观察者模式实现已经完成了。但在高性能、高并发的实际项目中我们还需要考虑更多。4.1 处理通知顺序与优先级默认情况下观察者被通知的顺序就是它们被注册的顺序std::vector的遍历顺序。但有时业务上需要优先级。我们可以修改attach方法接受一个优先级参数并在内部使用一个按优先级排序的容器比如std::multimap或带排序的std::vector。template typename EventT class PrioritySubject { public: using Priority int; // 优先级数字越小优先级越高 using ObserverItem std::pairPriority, ObserverWeakPtrEventT; ObserverPtrEventT attach(ObserverEventT observer, Priority priority 0) { auto observerPtr std::make_sharedObserverEventT(std::move(observer)); { std::lock_guardstd::mutex lock(mutex_); // 按优先级插入同优先级按插入时间此处为插入位置 observers_.emplace_back(priority, observerPtr); // 每次插入后排序不是最高效的可以改为在notify时排序或使用有序容器。 std::stable_sort(observers_.begin(), observers_.end(), [](const ObserverItem a, const ObserverItem b) { return a.first b.first; }); } return observerPtr; } // ... 其他方法需要相应调整比如detach和notify需要处理pair结构 private: mutable std::mutex mutex_; std::vectorObserverItem observers_; };注意在每次attach后都进行全排序在观察者数量多、注册频繁的场景下性能较差。更优的方案是使用std::multimapPriority, ObserverWeakPtrEventT它本身就能保持键值有序。但需要注意multimap的迭代器稳定性问题以及在多线程下修改结构的复杂性。4.2 异步通知在某些场景下我们可能希望主题在notify时不要阻塞当前线程而是将通知任务抛到另一个线程去异步执行。这可以防止耗时的观察者回调拖慢主题的状态更新流程。我们可以结合C11的std::async或线程池来实现。下面是一个使用std::async进行异步通知的简化示例template typename EventT void SubjectEventT::notifyAsync(const EventT event) { std::vectorObserverPtrEventT validObservers; { std::lock_guardstd::mutex lock(mutex_); // ... 同样的清理和收集逻辑 observers_.erase(std::remove_if(...), observers_.end()); for (const auto weakObserver : observers_) { if (auto strongObserver weakObserver.lock()) { validObservers.push_back(strongObserver); } } } // 为每个观察者启动一个异步任务 std::vectorstd::futurevoid futures; futures.reserve(validObservers.size()); for (const auto observer : validObservers) { futures.emplace_back(std::async(std::launch::async, [observer, event]() { try { (*observer)(event); } catch (...) { // 处理异常 } })); } // 可以选择等待所有异步任务完成也可以不等待fire-and-forget。 // 这里等待只是为了示例实际中可能不需要。 for (auto fut : futures) { fut.wait(); // 或者使用fut.get()来获取异常 } }重要提醒异步通知引入了新的复杂性。观察者回调的执行顺序无法保证且它们可能并发执行因此观察者的实现必须是线程安全的。此外大量频繁的异步任务创建和销毁开销很大在生产环境中务必使用线程池来管理这些任务而不是为每个通知都创建新线程。4.3 使用std::shared_mutexC17优化读多写少的场景在我们的实现中notify读操作和attach/detach写操作使用了同一个互斥锁std::mutex。这是一种保守但安全的做法。然而在观察者列表不常变化写操作少但通知非常频繁读操作极多的场景下这会造成不必要的竞争影响notify的性能。C17引入了std::shared_mutex共享互斥量它支持“共享锁”多个线程可以同时读和“独占锁”只有一个线程可以写。我们可以利用它来优化#include shared_mutex template typename EventT class OptimizedSubject { public: ObserverPtrEventT attach(ObserverEventT observer) { auto observerPtr std::make_sharedObserverEventT(std::move(observer)); { std::unique_lockstd::shared_mutex lock(mutex_); // 写操作用unique_lock observers_.emplace_back(observerPtr); } return observerPtr; } void notify(const EventT event) { std::vectorObserverPtrEventT validObservers; { std::shared_lockstd::shared_mutex lock(mutex_); // 读操作用shared_lock // 注意shared_lock下不能修改容器所以不能在这里执行erase清理。 // 我们需要先收集但失效的weak_ptr也会被收集在lock外调用前检查。 validObservers.reserve(observers_.size()); for (const auto weakObserver : observers_) { if (auto strongObserver weakObserver.lock()) { validObservers.push_back(strongObserver); } } } // 读锁释放 // 调用观察者 for (const auto observer : validObservers) { try { (*observer)(event); } catch (...) { /* ... */ } } // 惰性清理在下次notify或单独调用清理方法时用写锁进行。 // 可以引入一个计数器每N次通知后清理一次避免每次读都要写的冲突。 cleanupIfNeeded(); } private: void cleanupIfNeeded() { static std::atomicint callCount{0}; if (callCount % 100 0) { // 每100次通知清理一次 std::unique_lockstd::shared_mutex lock(mutex_); observers_.erase(std::remove_if(observers_.begin(), observers_.end(), [](const auto wp) { return wp.expired(); }), observers_.end()); } } mutable std::shared_mutex mutex_; std::vectorObserverWeakPtrEventT observers_; };这个优化在观察者数量庞大、通知极其频繁的系统中能带来显著的性能提升。代价是代码逻辑变得更复杂一些并且清理策略需要精心设计以避免脏数据积累过多。5. 常见问题、陷阱与调试技巧即使有了一个健壮的实现在实际使用观察者模式时仍然会遇到不少坑。这里记录一些我踩过的雷和解决方法。5.1 生命周期管理谁该持有谁的指针这是最核心的问题。我们的实现中主题持有观察者的weak_ptr外部用户比如创建观察者的模块持有观察者的shared_ptr即attach的返回值。这个shared_ptr是观察者回调对象的唯一所有者。陷阱如果外部用户过早释放了shared_ptr那么观察者回调对象就被销毁了主题内部的weak_ptr会失效这是正常行为。陷阱如果外部用户没有保存attach返回的shared_ptr那么这个临时shared_ptr在语句结束后就被销毁观察者会立即失效导致永远收不到通知。务必保存好attach的返回值最佳实践通常将返回的ObserverPtr作为观察者对象的成员变量保存在观察者对象的析构函数中调用主题的detach方法如果主题还存活。这实现了自动化的注册与反注册。class MyController { public: MyController(SubjectMyEvent subject) : subject_(subject) { // 注册并保存token observerToken_ subject_.attach([this](const MyEvent e) { this-handleEvent(e); }); } ~MyController() { // 反注册 subject_.detach(observerToken_); } private: void handleEvent(const MyEvent e) { /* ... */ } SubjectMyEvent subject_; ObserverPtrMyEvent observerToken_; // 关键 };5.2 在回调中再次修改观察者列表这是一个典型的递归锁或死锁场景。如果一个观察者的回调函数内部又调用了同一个主题的attach或detach方法而我们的mutex_不是递归锁std::mutex不是那么程序会死锁。解决方案1不推荐使用std::recursive_mutex。但这会隐藏设计问题并可能带来性能开销和复杂性。解决方案2推荐严格禁止在观察者回调中同步修改其所属的主题的观察者列表。如果确实需要可以将修改操作“延迟”执行。例如在回调中只是将一个修改请求放入一个队列主题在完成本次notify的所有回调遍历后再去处理这个队列。这需要更复杂的状态管理。我们的实现中notify方法在调用回调前已经释放了锁所以观察者回调中调用attach是安全的因为attach会重新获取锁。但是如果回调中调用的是detach自己而detach需要遍历列表查找这可能会破坏notify中正在进行的迭代器虽然我们已经收集了强引用但detach会修改原始列表。所以最安全的做法依然是约定不要在回调中修改当前主题的观察者列表。5.3 性能瓶颈与优化点锁竞争这是多线程下最主要的瓶颈。优化方法包括使用读写锁shared_mutex、减小锁的粒度如分片、或使用无锁数据结构难度极高。对于我们这个模式使用shared_mutex并配合先收集后回调的策略在大多数场景下已经足够。weak_ptr的lock()开销lock()是一个原子操作有一定开销。在观察者数量很多时遍历并lock每个weak_ptr的成本不容忽视。如果观察者的生命周期和主题紧密绑定且不会先于主题销毁可以考虑在调试稳定后在性能关键路径上冒险使用shared_ptr并仔细管理生命周期或者使用其他ID机制来管理观察者。动态内存分配每次attach都涉及创建shared_ptr和weak_ptr以及可能的容器扩容。对于高频注册/注销的场景可以考虑使用对象池来复用function对象的内存或者使用固定大小的环形缓冲区。5.4 调试技巧观察者不生效怎么办当发现事件发出了但观察者没反应时可以按以下步骤排查检查attach返回值是否被保存这是最常见的原因。没有保存ObserverPtr回调对象立刻被销毁。检查观察者生命周期确保发出notify时观察者对象如果是绑定成员函数或者捕获了上下文资源的lambda还活着。检查事件类型是否匹配std::function对参数类型要求严格。如果Subjectint的观察者注册了一个void(double)的函数编译不会报错因为模板和std::function的构造是宽松的但在notify调用时会发生类型转换错误或静默失败。确保事件类型严格匹配。在notify方法中添加调试日志打印出当前观察者列表的有效数量以及每次尝试调用前后的信息看是列表空了还是调用过程出错了。检查多线程时序问题是否有可能在notify遍历的过程中另一个线程刚好detach了某个观察者我们的实现先收集强引用可以避免迭代器失效但收集之后、调用之前如果观察者被detach并销毁我们仍然会调用一个已销毁对象的函数因为强引用shared_ptr还保持着对象存活。这强调了detach需要同步或者确保业务逻辑上不会出现这种极端竞争。最后我个人在大型项目中更倾向于使用一个中心化的事件总线Event Bus来管理多个主题和观察者上述的Subject类可以作为事件总线中针对某一类事件的通道实现。这样架构更清晰也便于进行全局的监控和管理。但无论如何这个基于C11现代特性的观察者模式核心实现都是构建更复杂事件系统的一块坚实、可靠的基石。