1. 从面试题看观察者模式的深度考察
"说说C++观察者模式"这个看似基础的问题,在实际面试中往往成为区分候选人的分水岭。最近一位朋友在字节面试中回答了7个要点仍被评价"不够深入",这反映出大厂对设计模式的考察早已超越了UML图和简单实现。真正资深的C++开发者需要从语言特性、系统设计和工程实践三个维度来剖析这个经典模式。
观察者模式的核心是建立对象间一对多的动态依赖关系,当一个对象(Subject)状态变化时,所有依赖它的观察者(Observer)都会自动收到通知。但在C++语境下,这个简单的定义背后隐藏着内存管理、线程安全、异常处理等复杂问题。下面我们就从实现细节到工程实践,拆解观察者模式在C++中的完整知识图谱。
2. 基础实现与内存管理陷阱
2.1 经典UML结构与C++实现
标准的观察者模式包含两个核心接口:
cpp复制class Observer {
public:
virtual ~Observer() = default;
virtual void update(Subject* subject) = 0;
};
class Subject {
private:
std::vector<Observer*> observers_;
public:
void attach(Observer* observer) {
observers_.push_back(observer);
}
void detach(Observer* observer) {
observers_.erase(
std::remove(observers_.begin(), observers_.end(), observer),
observers_.end());
}
void notify() {
for (auto observer : observers_) {
observer->update(this);
}
}
};
这个基础实现已经暴露了第一个严重问题:原始指针管理带来的内存安全隐患。当Observer对象被提前销毁但未从Subject中detach时,后续notify操作将导致野指针访问。
2.2 智能指针解决方案对比
现代C++提供了多种智能指针方案,各有适用场景:
| 方案类型 | 内存所有权 | 循环引用风险 | 线程安全 | 典型应用场景 |
|---|---|---|---|---|
| shared_ptr | 共享 | 高 | 是 | 多观察者共享状态 |
| weak_ptr | 弱引用 | 无 | 是 | 解决循环引用 |
| unique_ptr | 独占 | 无 | 否 | 单一观察者独占资源 |
改进后的attach方法示例:
cpp复制void attach(std::shared_ptr<Observer> observer) {
observers_.push_back(std::move(observer));
}
关键经验:在性能敏感场景中,原始指针+显式生命周期管理可能是更优选择。智能指针不是银弹,需要根据具体场景权衡。
3. 线程安全深度剖析
3.1 多线程环境下的竞态条件
当观察者模式运行在多线程环境时,至少存在三种竞态条件:
- 在notify过程中有新观察者attach/detach
- 观察者的update方法被并发调用
- Subject状态变更与通知发送非原子操作
3.2 锁粒度优化策略
简单的全局锁会导致性能瓶颈,我们需要更精细的加锁策略:
cpp复制class ThreadSafeSubject {
private:
std::vector<std::shared_ptr<Observer>> observers_;
mutable std::mutex mtx_;
public:
void attach(std::shared_ptr<Observer> observer) {
std::lock_guard<std::mutex> lock(mtx_);
observers_.push_back(std::move(observer));
}
void notify() {
std::vector<std::shared_ptr<Observer>> local_copy;
{
std::lock_guard<std::mutex> lock(mtx_);
local_copy = observers_;
}
for (auto& observer : local_copy) {
if (auto ptr = observer.lock()) {
ptr->update(this);
}
}
}
};
这种实现的特点:
- 使用lock_guard保证单个操作的原子性
- notify时先复制观察者列表再遍历,减少临界区持有时间
- 配合weak_ptr防止观察者失效
3.3 无锁方案探索
对于极端性能要求的场景,可以考虑无锁队列方案。以下是基于atomic_flag的简单实现:
cpp复制class LockFreeSubject {
private:
std::atomic<ObserverList*> observers_{nullptr};
ObserverList* acquire_list() {
ObserverList* current = observers_.load();
ObserverList* new_list = new ObserverList(*current);
while (!observers_.compare_exchange_weak(current, new_list)) {
delete new_list;
new_list = new ObserverList(*current);
}
return current;
}
public:
void notify() {
ObserverList* current = acquire_list();
for (auto& observer : *current) {
observer->update(this);
}
delete current;
}
};
4. 异常安全与资源管理
4.1 通知过程中的异常传播
当观察者的update方法抛出异常时,会导致:
- 后续观察者无法收到通知
- 可能破坏Subject的内部状态
解决方案示例:
cpp复制void safe_notify() {
std::vector<std::exception_ptr> exceptions;
for (auto& observer : observers_) {
try {
observer->update(this);
} catch (...) {
exceptions.push_back(std::current_exception());
}
}
if (!exceptions.empty()) {
throw AggregateException(exceptions);
}
}
4.2 资源泄漏防护
即使使用智能指针,仍可能因循环引用导致内存泄漏。典型场景:
cpp复制class Form : public Observer {
private:
std::shared_ptr<Button> button_; // 持有Subject的shared_ptr
public:
Form(std::shared_ptr<Button> btn) : button_(btn) {
button_->attach(shared_from_this()); // 形成循环引用
}
};
解决方案是使用weak_ptr打破循环:
cpp复制class SafeForm : public std::enable_shared_from_this<SafeForm>, public Observer {
private:
std::weak_ptr<Button> button_;
public:
SafeForm(std::shared_ptr<Button> btn) : button_(btn) {
if (auto ptr = button_.lock()) {
ptr->attach(shared_from_this());
}
}
};
5. 性能优化实战技巧
5.1 通知频率控制
高频通知会导致性能问题,常见优化策略:
- 批量通知模式
cpp复制void set_value(int new_val) {
value_ = new_val;
if (++change_count_ > threshold_) {
notify();
change_count_ = 0;
}
}
- 异步通知队列
cpp复制void async_notify() {
std::async(std::launch::async, [this] {
std::lock_guard<std::mutex> lock(queue_mtx_);
notification_queue_.push_back([this] { notify(); });
if (!notifying_) {
notifying_ = true;
process_queue();
}
});
}
void process_queue() {
while (!notification_queue_.empty()) {
auto task = notification_queue_.front();
notification_queue_.pop_front();
task();
}
notifying_ = false;
}
5.2 观察者优先级系统
通过优先级控制通知顺序:
cpp复制struct ObserverRecord {
std::weak_ptr<Observer> observer;
int priority = 0;
bool operator<(const ObserverRecord& other) const {
return priority > other.priority; // 降序排列
}
};
void notify() {
std::vector<ObserverRecord> active_observers;
{
std::lock_guard<std::mutex> lock(mtx_);
std::copy_if(observers_.begin(), observers_.end(),
std::back_inserter(active_observers),
[](const auto& record) {
return !record.observer.expired();
});
std::sort(active_observers.begin(), active_observers.end());
}
for (const auto& record : active_observers) {
if (auto observer = record.observer.lock()) {
observer->update(this);
}
}
}
6. 现代C++特性应用
6.1 使用function代替接口
C++11后的更灵活实现:
cpp复制class CallbackSubject {
private:
std::vector<std::function<void(Subject*)>> callbacks_;
public:
template <typename F>
void register_callback(F&& f) {
callbacks_.emplace_back(std::forward<F>(f));
}
void notify() {
for (auto& cb : callbacks_) {
cb(this);
}
}
};
优势:
- 无需继承Observer接口
- 支持lambda表达式
- 更好的内联优化机会
6.2 变参模板支持
通用观察者模式实现:
cpp复制template <typename... Args>
class GenericObserver {
public:
virtual void update(Args...) = 0;
};
template <typename... Args>
class GenericSubject {
private:
std::vector<std::function<void(Args...)>> observers_;
public:
void attach(std::function<void(Args...)> observer) {
observers_.push_back(std::move(observer));
}
void notify(Args... args) {
for (auto& observer : observers_) {
observer(args...);
}
}
};
7. 工程实践中的设计考量
7.1 接口设计原则
良好的观察者模式接口应该:
- 明确通知语义(同步/异步)
- 定义明确的状态传递方式
- 处理观察者生命周期问题
- 考虑批量更新场景
7.2 测试策略
针对观察者模式的特殊测试场景:
- 观察者销毁后Subject的行为
- 高频通知下的性能表现
- 多线程竞争条件下的稳定性
- 异常抛出时的系统状态
7.3 典型应用场景分析
| 应用场景 | 技术要点 | 常见陷阱 |
|---|---|---|
| GUI事件处理 | 高频低延迟通知 | 事件堆积导致界面卡顿 |
| 分布式系统监控 | 网络延迟容忍 | 观察者失联处理 |
| 游戏引擎 | 多线程环境下的状态同步 | 竞态条件导致状态不一致 |
| 金融交易系统 | 严格的有序通知 | 通知丢失导致交易错误 |
在实现一个生产级可用的观察者模式时,我通常会采用分阶段验证策略:先确保单线程下的基本功能,再逐步添加线程安全、异常处理等特性,最后进行性能优化。这种渐进式的方法能有效降低复杂度,每个阶段都可以进行针对性的测试和验证。
