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 混合使用策略

实际项目中,我很少只用一种模式。一个典型的嵌入式系统往往是多种模式混搭:

  1. 中断层:直接分发。中断服务函数里只做最必要的事,直接调用处理函数。
  2. 任务层:点对点模式。任务间通过消息队列通信,每个任务只处理自己的消息。
  3. 模块层:订阅-发布模式。业务模块之间通过事件总线解耦。
  4. 系统层:广播分发。系统级事件(如掉电预警)需要所有模块协同响应。
重要提醒:不要在中断服务函数里使用订阅-发布模式!我曾经在一个项目中,ISR 里调用了 publish 函数,结果 publish 里遍历了 30 个订阅者,每个订阅者又去获取互斥锁……死锁了。记住:ISR 里只做直接分发,或者只发信号量通知任务。
事件分发机制选型决策树 事件分发需求 中断上下文? 任务/模块上下文? 直接分发 一对多通信? 点对点模式 订阅-发布模式 广播分发 选型建议: • 中断里只用直接分发,别碰其他模式 • 任务间通信首选点对点,用消息队列实现 • 模块解耦用订阅-发布,但要控制订阅者数量 • 系统级事件用广播,但要防止惊群效应

嗯,四种模式讲完了。你可能会问:「那我到底该用哪种?」我的答案是:先画通信拓扑图。把系统中谁产生事件、谁消费事件、有多少个消费者、实时性要求多高,全部列出来。然后对着这张图选模式——你会发现,大部分场景的答案其实很清晰。