4. 事件分发机制:直接分发、广播分发、订阅-发布模式、点对点模式
事件分发,说白了就是事件从产生到被消费的「运输路线」。选错了分发方式,轻则浪费 CPU,重则系统崩溃。我在项目中见过太多因为分发机制选型不当导致的性能灾难——嗯,今天咱们就把这四种模式掰开揉碎了讲清楚。
核心观点:没有最好的分发模式,只有最合适的场景。选型的关键在于「谁需要知道这个事件」以及「需要多快知道」。
4.1 直接分发——最原始,也最高效
直接分发,就是事件源直接调用事件处理函数。没有中间商赚差价,延迟最低。
// 直接分发示例
typedef struct {
void (*on_button_pressed)(void);
void (*on_timer_expired)(void);
} event_handlers_t;
event_handlers_t handlers;
void dispatch_event(uint32_t event_id) {
switch (event_id) {
case EVENT_BUTTON_PRESSED:
if (handlers.on_button_pressed) {
handlers.on_button_pressed(); // 直接调用
}
break;
case EVENT_TIMER_EXPIRED:
if (handlers.on_timer_expired) {
handlers.on_timer_expired(); // 直接调用
}
break;
}
}
优点:零开销,执行路径最短。
缺点:耦合度高,扩展性差。每增加一个事件类型,都要改 dispatch 函数。
我的经验:在中断服务函数里,我几乎只用直接分发。因为中断上下文对延迟极度敏感,任何间接调用都可能引入不可预测的抖动。我曾经在一个电机控制项目里,因为用了订阅-发布模式处理霍尔传感器中断,导致换相延迟增加了 3μs,电机直接抖成了拖拉机。
4.2 广播分发——一呼百应,但小心风暴
广播分发,就是事件源把事件扔给所有注册的接收者。谁想听谁就自己处理。
// 广播分发示例
#define MAX_LISTENERS 16
typedef void (*event_callback_t)(uint32_t event_id, void *data);
typedef struct {
event_callback_t listeners[MAX_LISTENERS];
uint8_t count;
} broadcaster_t;
void broadcast_event(broadcaster_t *bc, uint32_t event_id, void *data) {
for (uint8_t i = 0; i < bc->count; i++) {
if (bc->listeners[i]) {
bc->listeners[i](event_id, data);
}
}
}
适用场景:系统状态变化(如网络断开、电源切换),需要多个模块同时响应。
避坑指南:我曾经在一个物联网网关项目里,把心跳超时事件做成了广播分发。结果 8 个模块同时去重连网络、刷新缓存、上报日志……CPU 占用率瞬间飙到 95%,看门狗直接复位。广播分发一定要警惕「惊群效应」——多个接收者做重复工作,白白浪费算力。
4.3 订阅-发布模式——解耦的利器
这是我最喜欢用的模式。发布者和订阅者完全不知道对方的存在,只通过一个事件通道通信。
// 订阅-发布模式核心实现
typedef struct subscriber_node {
uint32_t event_type;
event_callback_t callback;
struct subscriber_node *next;
} subscriber_node_t;
typedef struct {
subscriber_node_t *head;
uint8_t priority; // 0-255, 越小优先级越高
} event_channel_t;
void publish(event_channel_t *channel, uint32_t event_type, void *data) {
subscriber_node_t *node = channel->head;
while (node) {
if (node->event_type == event_type || node->event_type == EVENT_ALL) {
node->callback(event_type, data);
}
node = node->next;
}
}
void subscribe(event_channel_t *channel, uint32_t event_type,
event_callback_t cb) {
subscriber_node_t *node = malloc(sizeof(subscriber_node_t));
node->event_type = event_type;
node->callback = cb;
node->next = channel->head;
channel->head = node;
}
优势:
- 模块间零耦合,新增功能不需要改现有代码
- 支持一对多、多对一、多对多通信
- 可以动态订阅/取消订阅
代价:每次发布都要遍历订阅链表,时间复杂度 O(n)。n 大了就是灾难。
性能调优要点:订阅链表用「头插法」——新订阅者插在头部。因为大多数场景下,最近订阅的模块最活跃,这样遍历时能更快命中。
4.4 点对点模式——精准投递
点对点模式,就是事件有明确的「收件人」。事件通道只把事件发给指定的接收者。
// 点对点模式示例
typedef struct {
uint32_t dest_task_id;
uint32_t event_id;
uint32_t param;
} p2p_message_t;
// 发送端
void send_message(uint32_t dest_task_id, uint32_t event_id, uint32_t param) {
p2p_message_t msg = {
.dest_task_id = dest_task_id,
.event_id = event_id,
.param = param
};
// 直接放入目标任务的消息队列
task_post_message(dest_task_id, &msg);
}
// 接收端
void task_main(void) {
p2p_message_t msg;
while (1) {
if (task_get_message(&msg) == SUCCESS) {
process_event(msg.event_id, msg.param);
}
}
}
典型应用:多任务系统中的任务间通信。每个任务有自己的消息队列,事件直接投递到目标队列。
我的习惯:在 FreeRTOS 项目中,我通常用消息队列实现点对点通信。每个任务的消息队列长度设为 8-16 个槽位,既不会浪费内存,又能应对突发事件。我曾经见过有人给每个任务分配 256 个槽位——结果 10 个任务吃掉了 40KB RAM,而实际使用率不到 5%。
4.5 四种模式对比
| 特性 | 直接分发 | 广播分发 | 订阅-发布 | 点对点 |
|---|---|---|---|---|
| 延迟 | 最低 | 低 | 中 | 低 |
| 耦合度 | 高 | 中 | 低 | 中 |
| 扩展性 | 差 | 好 | 最好 | 好 |
| 内存开销 | 零 | 低 | 中 | 中 |
| 适用场景 | 中断、高频事件 | 系统状态变化 | 模块间解耦 | 任务间通信 |
4.6 混合使用策略
实际项目中,我很少只用一种模式。一个典型的嵌入式系统往往是多种模式混搭:
- 中断层:直接分发。中断服务函数里只做最必要的事,直接调用处理函数。
- 任务层:点对点模式。任务间通过消息队列通信,每个任务只处理自己的消息。
- 模块层:订阅-发布模式。业务模块之间通过事件总线解耦。
- 系统层:广播分发。系统级事件(如掉电预警)需要所有模块协同响应。
重要提醒:不要在中断服务函数里使用订阅-发布模式!我曾经在一个项目中,ISR 里调用了 publish 函数,结果 publish 里遍历了 30 个订阅者,每个订阅者又去获取互斥锁……死锁了。记住:ISR 里只做直接分发,或者只发信号量通知任务。
嗯,四种模式讲完了。你可能会问:「那我到底该用哪种?」我的答案是:先画通信拓扑图。把系统中谁产生事件、谁消费事件、有多少个消费者、实时性要求多高,全部列出来。然后对着这张图选模式——你会发现,大部分场景的答案其实很清晰。