做DSP开发或嵌入式编程的同仁,大概率都碰到过这类场景:中断触发后,得同步完成显示更新、日志记录、后续控制逻辑触发;状态机切换状态时,多个模块要根据新状态执行对应操作。要是直接把这些逻辑硬编码到中断服务函数或状态切换函数里,后续新增功能就得反复修改核心代码,不仅容易引入bug,维护起来也格外棘手。
其实,解决这类"一个事件触发多个后续动作"的问题,有个经典且实用的设计模式------观察者模式。今天,我就按"原理拆解→工程化分析→C语言实现→实战验证→问题解决"的逻辑,手把手带大家吃透这个模式,帮你写出更具扩展性、可维护性的嵌入式代码。
一、原理拆解:观察者模式的核心逻辑
观察者模式的核心思想很直白:定义对象间的一对多依赖关系,当被观察对象(我们称之为"主题")的状态发生变化时,会自动通知所有依赖它的对象(我们称之为"观察者"),并驱动观察者执行相应的更新操作。
用通俗的话讲,这就像我们订阅报纸:你(观察者)向报社(主题)订阅报纸,一旦报社有新报纸出版(状态变化),就会主动把报纸送到所有订阅者手上,你完全不用每天跑报社询问。这里的关键是"主动通知"和"解耦"------订阅者和报社之间没有强绑定,订阅者可随时取消订阅,报社也不用关心订阅者拿到报纸后具体怎么处理。
从结构上看,观察者模式主要包含两个核心角色:
-
主题(Subject):作为被观察的核心对象,核心职责有三个:维护一个观察者链表,提供观察者的注册(订阅)、注销(取消订阅)接口,以及在自身状态变化时,遍历观察者链表并通知所有观察者执行更新操作。
-
观察者(Observer):作为依赖主题的对象,必须定义一个统一的更新接口。当收到主题的通知时,通过这个接口执行具体的业务逻辑。不同观察者可实现不同的更新逻辑,从而实现功能的差异化扩展。
通过这两个角色的配合,观察者模式实现了"主题"与"观察者"的解耦:主题不用知道具体有哪些观察者,也不用关心观察者的具体实现;观察者只需通过统一接口注册到主题,就能被动接收通知。这种解耦特性让两者可以独立迭代更新,大幅提升代码的扩展性。
二、工程化分析:为什么嵌入式开发需要观察者模式?
可能有朋友会疑惑:嵌入式开发讲究轻量、高效,直接用函数回调不也能实现类似功能吗?为啥非要用观察者模式?其实在工程化落地场景中,观察者模式相比直接写回调,有三个不可替代的优势:
-
解耦核心逻辑与扩展逻辑:以中断事件处理为例,中断触发是核心逻辑,日志记录、显示更新、控制逻辑属于扩展逻辑。用观察者模式后,中断服务函数只需负责通知观察者,无需关心具体的扩展逻辑;后续新增扩展功能时,只需新增观察者并注册,不用修改中断核心代码,完全符合"开闭原则"。
-
统一管理依赖关系:如果直接用多个回调函数,需要手动维护回调函数指针数组,注册、注销逻辑容易混乱。观察者模式通过主题统一管理观察者链表,接口清晰规范,能有效避免分散管理带来的bug。
-
支持动态扩展与灵活配置:嵌入式设备在不同场景下可能需要不同的功能组合,比如调试模式下需要日志记录,正常运行模式下则不需要。借助观察者模式,可动态注册/注销观察者,实现功能的灵活切换,无需重新编译代码。
当然,观察者模式也不是万能的。在嵌入式场景中使用时,有两个问题需要重点关注:一是线程安全问题,尤其是在多线程或中断上下文通知观察者时,容易出现链表操作冲突;二是性能问题,若观察者过多或更新逻辑复杂,会导致通知耗时过长,影响系统实时性,同时还要避免主题与观察者之间出现循环依赖。这些问题,我们后面会给出具体的解决方案。
三、C语言实现:手把手写一个基础版本
C语言没有类和对象的概念,但我们可以用结构体模拟"主题"和"观察者",用函数指针实现统一的更新接口。下面,我们就一步步实现一个基础版本的观察者模式,帮大家理解核心实现逻辑。
3.1 定义核心结构体与接口
首先定义观察者结构体,核心是一个更新函数指针,用于让不同观察者实现差异化的更新逻辑;接着定义主题结构体,包含观察者链表、链表当前长度、最大容量,以及注册、注销、通知三大核心接口的函数指针。
c
#include <stdio.h>
#include <stdlib.h>
// 前向声明主题结构体,观察者的更新函数需要用到
typedef struct Subject Subject;
// 观察者结构体:核心是统一的更新接口(函数指针)
typedef struct Observer {
// 更新函数:参数为主题指针(可获取主题状态)和自定义数据
void (*update)(Subject* subject, void* data);
// 用于区分不同观察者,方便注销
int id;
} Observer;
// 主题结构体:维护观察者链表,提供注册/注销/通知接口
typedef struct Subject {
// 观察者链表(动态数组,适应灵活增减)
Observer** observers;
// 当前观察者数量
int observer_count;
// 链表最大容量(避免频繁扩容,提升性能)
int max_observers;
// 主题状态(示例:用整数表示,实际可根据需求定义)
int state;
// 接口函数指针
// 注册观察者
int (*register_observer)(Subject* subject, Observer* observer);
// 注销观察者
int (*unregister_observer)(Subject* subject, int observer_id);
// 通知所有观察者
void (*notify_observers)(Subject* subject, void* data);
} Subject;
3.2 实现主题的核心接口
接下来实现主题的注册、注销、通知这三个核心函数。注册函数负责将观察者添加到链表中,注销函数负责移除指定ID的观察者,通知函数负责遍历观察者链表,调用每个观察者的更新函数。
c
// 注册观察者
static int subject_register_observer(Subject* subject, Observer* observer) {
if (subject == NULL || observer == NULL) {
printf("Error: subject or observer is NULL\n");
return -1;
}
// 检查观察者是否已注册(避免重复注册)
for (int i = 0; i < subject->observer_count; i++) {
if (subject->observers[i]->id == observer->id) {
printf("Warning: observer %d already registered\n", observer->id);
return 0;
}
}
// 链表满了,扩容(这里简单扩容为原来的2倍,可根据需求调整)
if (subject->observer_count >= subject->max_observers) {
int new_max = subject->max_observers * 2;
Observer** new_observers = realloc(subject->observers, new_max * sizeof(Observer*));
if (new_observers == NULL) {
printf("Error: realloc failed\n");
return -1;
}
subject->observers = new_observers;
subject->max_observers = new_max;
printf("Info: subject observers list expanded to %d\n", new_max);
}
// 添加观察者到链表
subject->observers[subject->observer_count++] = observer;
printf("Info: observer %d registered successfully\n", observer->id);
return 0;
}
// 注销观察者
static int subject_unregister_observer(Subject* subject, int observer_id) {
if (subject == NULL) {
printf("Error: subject is NULL\n");
return -1;
}
// 查找观察者在链表中的位置
int index = -1;
for (int i = 0; i < subject->observer_count; i++) {
if (subject->observers[i]->id == observer_id) {
index = i;
break;
}
}
if (index == -1) {
printf("Warning: observer %d not found\n", observer_id);
return -1;
}
// 移除观察者(将后面的元素向前移动一位)
for (int i = index; i < subject->observer_count - 1; i++) {
subject->observers[i] = subject->observers[i + 1];
}
subject->observer_count--;
printf("Info: observer %d unregistered successfully\n", observer_id);
return 0;
}
// 通知所有观察者
static void subject_notify_observers(Subject* subject, void* data) {
if (subject == NULL) {
printf("Error: subject is NULL\n");
return;
}
// 遍历观察者链表,调用每个观察者的更新函数
for (int i = 0; i < subject->observer_count; i++) {
if (subject->observers[i]->update != NULL) {
subject->observers[i]->update(subject, data);
}
}
}
// 初始化主题
Subject* subject_init(int max_observers) {
if (max_observers <= 0) {
printf("Error: max_observers must be positive\n");
return NULL;
}
Subject* subject = malloc(sizeof(Subject));
if (subject == NULL) {
printf("Error: malloc subject failed\n");
return NULL;
}
// 初始化观察者链表
subject->observers = malloc(max_observers * sizeof(Observer*));
if (subject->observers == NULL) {
printf("Error: malloc observers list failed\n");
free(subject);
return NULL;
}
subject->observer_count = 0;
subject->max_observers = max_observers;
subject->state = 0; // 初始状态为0
// 绑定接口函数
subject->register_observer = subject_register_observer;
subject->unregister_observer = subject_unregister_observer;
subject->notify_observers = subject_notify_observers;
printf("Info: subject initialized successfully\n");
return subject;
}
// 销毁主题(释放资源)
void subject_destroy(Subject* subject) {
if (subject == NULL) {
return;
}
if (subject->observers != NULL) {
free(subject->observers);
}
free(subject);
printf("Info: subject destroyed successfully\n");
}
3.3 实现观察者的更新逻辑
观察者需要实现具体的更新函数,不同观察者可根据业务需求实现不同的更新逻辑。这里我们以嵌入式开发中常见的"中断事件通知"场景为例,实现两个典型观察者:一个负责记录中断日志,一个负责更新显示界面。
c
// 观察者1:记录日志
void log_observer_update(Subject* subject, void* data) {
if (subject == NULL || data == NULL) {
return;
}
int event_type = *(int*)data;
printf("Log Observer: Interrupt event %d triggered, subject state: %d\n", event_type, subject->state);
}
// 观察者2:更新显示
void display_observer_update(Subject* subject, void* data) {
if (subject == NULL || data == NULL) {
return;
}
int event_type = *(int*)data;
printf("Display Observer: Show interrupt event %d, current state: %d\n", event_type, subject->state);
}
// 初始化观察者
Observer* observer_init(int id, void (*update)(Subject* subject, void* data)) {
if (update == NULL) {
printf("Error: update function is NULL\n");
return NULL;
}
Observer* observer = malloc(sizeof(Observer));
if (observer == NULL) {
printf("Error: malloc observer failed\n");
return NULL;
}
observer->id = id;
observer->update = update;
return observer;
}
// 销毁观察者
void observer_destroy(Observer* observer) {
if (observer != NULL) {
free(observer);
}
}
四、实战验证:中断事件通知场景测试
下面我们结合"中断事件触发"的实战场景,测试基础版本观察者模式的功能。假设外部中断触发时(主题状态发生变化),需要通知日志观察者记录事件、显示观察者更新界面,同时支持动态注销观察者。
c
int main() {
// 1. 初始化主题(最大支持4个观察者)
Subject* subject = subject_init(4);
if (subject == NULL) {
return -1;
}
// 2. 初始化两个观察者
Observer* log_observer = observer_init(1, log_observer_update);
Observer* display_observer = observer_init(2, display_observer_update);
if (log_observer == NULL || display_observer == NULL) {
observer_destroy(log_observer);
observer_destroy(display_observer);
subject_destroy(subject);
return -1;
}
// 3. 注册观察者
subject->register_observer(subject, log_observer);
subject->register_observer(subject, display_observer);
// 4. 模拟中断事件1触发(主题状态更新为1,通知观察者)
printf("\n--- Interrupt Event 1 Triggered ---\n");
subject->state = 1;
int event1 = 1;
subject->notify_observers(subject, &event1);
// 5. 注销显示观察者
subject->unregister_observer(subject, 2);
// 6. 模拟中断事件2触发(主题状态更新为2,通知观察者)
printf("\n--- Interrupt Event 2 Triggered ---\n");
subject->state = 2;
int event2 = 2;
subject->notify_observers(subject, &event2);
// 7. 释放资源
observer_destroy(log_observer);
observer_destroy(display_observer);
subject_destroy(subject);
return 0;
}
4.1 测试结果与分析
编译运行上述代码,输出结果如下。通过结果我们可以直观验证观察者模式的核心功能是否正常:
text
Info: subject initialized successfully
Info: observer 1 registered successfully
Info: observer 2 registered successfully
--- Interrupt Event 1 Triggered ---
Log Observer: Interrupt event 1 triggered, subject state: 1
Display Observer: Show interrupt event 1, current state: 1
Info: observer 2 unregistered successfully
--- Interrupt Event 2 Triggered ---
Log Observer: Interrupt event 2 triggered, subject state: 2
Info: subject destroyed successfully
从输出结果可以清晰看出:
-
两个观察者成功注册后,中断事件1触发时,两者都收到了主题的通知,并执行了各自的更新逻辑(记录日志、更新显示);
-
注销显示观察者后,中断事件2触发时,只有日志观察者收到通知并执行操作,实现了观察者的动态增减;
-
主题与观察者之间完全解耦,若后续需要新增"中断事件控制"功能,只需新增一个控制观察者并实现update函数,注册到主题即可,无需修改主题的核心代码。
五、问题解决:线程安全与性能优化
上面实现的基础版本,在单线程、非中断场景下可以正常使用。但在嵌入式实际开发中,多线程或中断上下文通知观察者的场景很常见,这时候就必须解决线程安全和性能优化问题,否则会导致程序运行异常。
5.1 线程安全实现
线程安全的核心问题是"并发修改观察者链表"------比如一个线程正在注册观察者(修改链表),另一个线程同时在通知观察者(遍历链表),很容易导致链表访问越界、数据错乱等问题。在嵌入式开发中,解决这个问题最常用的两种方式是:用互斥锁保护链表操作(针对多线程场景)、关闭中断保护(针对中断上下文场景)。
下面我们基于互斥锁优化主题的接口函数(以FreeRTOS的互斥锁为例,其他操作系统可替换为对应的锁机制,核心逻辑一致):
c
#include "FreeRTOS.h"
#include "semphr.h"
// 扩展主题结构体,添加互斥锁
typedef struct Subject {
Observer** observers;
int observer_count;
int max_observers;
int state;
SemaphoreHandle_t mutex; // 互斥锁
int (*register_observer)(Subject* subject, Observer* observer);
int (*unregister_observer)(Subject* subject, int observer_id);
void (*notify_observers)(Subject* subject, void* data);
} Subject;
// 初始化主题(添加互斥锁初始化)
Subject* subject_init(int max_observers) {
// 省略前面的初始化代码...
// 创建互斥锁
subject->mutex = xSemaphoreCreateMutex();
if (subject->mutex == NULL) {
printf("Error: create mutex failed\n");
free(subject->observers);
free(subject);
return NULL;
}
return subject;
}
// 优化注册函数,添加锁保护
static int subject_register_observer(Subject* subject, Observer* observer) {
if (subject == NULL || observer == NULL) {
return -1;
}
// 获取互斥锁(等待时间100ms,可根据需求调整)
if (xSemaphoreTake(subject->mutex, pdMS_TO_TICKS(100)) != pdPASS) {
printf("Error: take mutex failed for register\n");
return -1;
}
// 省略中间的注册逻辑...
// 释放互斥锁
xSemaphoreGive(subject->mutex);
return 0;
}
// 同理优化注销和通知函数,在访问链表前后加锁/解锁
static int subject_unregister_observer(Subject* subject, int observer_id) {
if (subject == NULL) {
return -1;
}
if (xSemaphoreTake(subject->mutex, pdMS_TO_TICKS(100)) != pdPASS) {
printf("Error: take mutex failed for unregister\n");
return -1;
}
// 省略中间的注销逻辑...
xSemaphoreGive(subject->mutex);
return 0;
}
static void subject_notify_observers(Subject* subject, void* data) {
if (subject == NULL) {
return;
}
if (xSemaphoreTake(subject->mutex, pdMS_TO_TICKS(100)) != pdPASS) {
printf("Error: take mutex failed for notify\n");
return;
}
// 省略遍历通知逻辑...
xSemaphoreGive(subject->mutex);
}
如果观察者模式需要在中断上下文使用,由于中断上下文不能使用阻塞式的互斥锁,此时建议采用"关闭中断保护"的方式,确保链表操作的原子性:
c
// 中断上下文的通知函数
static void subject_notify_observers_isr(Subject* subject, void* data) {
if (subject == NULL) {
return;
}
// 保存中断状态,关闭中断
UBaseType_t uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();
// 遍历观察者链表,通知观察者
for (int i = 0; i < subject->observer_count; i++) {
if (subject->observers[i]->update != NULL) {
subject->observers[i]->update(subject, data);
}
}
// 恢复中断状态
taskEXIT_CRITICAL_FROM_ISR(uxSavedInterruptStatus);
}
5.2 性能优化与循环依赖避免
在嵌入式实时系统中,观察者模式的性能优化核心是"保障实时性",同时要避免循环依赖导致的程序死循环。主要从以下两个方面入手:
-
控制观察者数量:根据实际业务需求设定合理的max_observers,避免观察者链表过长导致遍历耗时增加,影响系统实时性;
-
简化update函数逻辑:观察者的update函数要尽量简洁,避免在其中执行复杂计算、大量IO等耗时操作;若确实需要执行耗时操作,可通过消息队列将任务抛给后台线程处理,避免阻塞主题的通知流程;
-
使用静态链表替代动态链表:若观察者数量固定(如嵌入式设备的固定功能模块),可直接用静态数组实现观察者链表,避免malloc/realloc带来的性能开销和内存碎片问题。
-
减少通知耗时,保障实时性:
-
添加通知标记:在主题中增加"当前通知观察者ID"标记,通知过程中若再次触发主题状态变化,跳过当前正在通知的观察者;
-
明确职责边界:严格划分主题和观察者的职责,主题只负责状态管理和通知,观察者仅负责接收通知并执行自身逻辑,不允许观察者主动修改主题的状态。
-
-
避免循环依赖,防止死循环:循环依赖是指观察者A的update函数中,会触发主题的状态变化,而主题状态变化后又会再次通知观察者A,最终导致无限循环。解决方式主要有两种:
六、总结
总结一下,观察者模式通过"主题-观察者"的一对多依赖关系,实现了核心逻辑与扩展逻辑的解耦,非常适配嵌入式开发中"一个事件触发多个后续动作"的场景,比如中断事件通知、状态机状态变更回调、消息订阅/发布系统等。
今天我们从原理拆解、工程化价值分析,到手把手实现C语言基础版本、结合中断场景实战验证,最后解决了线程安全和性能优化这两个核心工程问题,相信大家已经掌握了观察者模式在嵌入式开发中的核心用法。实际项目中,大家可根据具体场景灵活调整实现细节,比如用静态链表替代动态链表、适配不同OS的锁机制等。
如果这篇文章对你的嵌入式开发工作有帮助,别忘了点赞、收藏,关注我!后续会持续分享更多嵌入式开发相关的设计模式实战、性能优化技巧、问题排查方法。如果在使用观察者模式的过程中遇到了具体问题,或者有其他想深入了解的技术点,欢迎在评论区留言讨论!