4、事件分发机制:中心化分发器 vs 订阅发布模式

事件分发,说白了就是解决「谁产生了事件,谁该处理它」这个问题。我在嵌入式项目里见过太多因为分发逻辑混乱导致的死锁、丢事件、优先级反转。今天咱们就把两种主流方案掰开揉碎讲清楚。

4.1 中心化分发器:一个调度员管所有

中心化分发器,就像一个总机接线员。所有事件都发给它,它再根据事件类型查表,转发给对应的处理函数。

核心特征:单一入口,统一路由,处理函数不直接感知事件源。

4.1.1 典型实现

// 事件分发器结构
typedef struct {
    uint32_t event_id;
    void (*handler)(void *data, uint32_t len);
} event_route_t;

// 路由表(静态数组)
static event_route_t route_table[] = {
    {EVT_BUTTON_PRESS,   button_press_handler},
    {EVT_SENSOR_READY,   sensor_data_handler},
    {EVT_TIMER_EXPIRED,  timer_handler},
    // ...
};

// 分发函数
void dispatcher_dispatch(uint32_t event_id, void *data, uint32_t len) {
    for (int i = 0; i < ROUTE_TABLE_SIZE; i++) {
        if (route_table[i].event_id == event_id) {
            route_table[i].handler(data, len);
            return;
        }
    }
    // 未注册事件——我习惯在这里打一个调试断言
    ASSERT(0);
}

这种模式的好处是简单、确定性强。我做过一个工业控制器的项目,所有IO事件都走中心化分发,调试时只要在分发函数里加一个打印,整个系统的事件流就一目了然。

注意:中心化分发器不适合高动态场景。如果事件类型经常增删,或者处理函数需要频繁切换,路由表维护起来会非常痛苦。

4.2 订阅发布模式:谁关心谁订阅

订阅发布模式,说白了就是「广播+订阅」。事件产生者只管发,不关心谁收;处理者只管订阅,不关心谁发。两者完全解耦。

4.2.1 核心机制

  • 发布者(Publisher):产生事件,调用发布接口
  • 订阅者(Subscriber):注册自己关心的事件类型
  • 事件总线(Event Bus):维护订阅关系,负责转发

我的经验:在物联网网关项目中,传感器模块、网络模块、日志模块各自独立,用订阅发布模式后,新增一个传感器类型只需要写订阅代码,完全不用改分发逻辑。

4.2.2 简单实现示例

// 订阅节点
typedef struct sub_node {
    uint32_t event_id;
    void (*handler)(void *data, uint32_t len);
    struct sub_node *next;
} sub_node_t;

// 事件总线
static sub_node_t *sub_list_head = NULL;

// 订阅接口
void event_bus_subscribe(uint32_t event_id, void (*handler)(void*, uint32_t)) {
    sub_node_t *node = malloc(sizeof(sub_node_t));
    node->event_id = event_id;
    node->handler = handler;
    node->next = sub_list_head;
    sub_list_head = node;  // 头插法
}

// 发布接口
void event_bus_publish(uint32_t event_id, void *data, uint32_t len) {
    sub_node_t *cur = sub_list_head;
    while (cur) {
        if (cur->event_id == event_id) {
            cur->handler(data, len);
        }
        cur = cur->next;
    }
}

4.3 两种模式对比

维度 中心化分发器 订阅发布模式
耦合度 中等(分发器知道所有处理者) 低(发布者和订阅者互不知晓)
实时性 高(查表直接调用) 中等(链表遍历有开销)
动态性 差(路由表需编译时确定) 好(运行时增删订阅)
调试难度 低(单一入口,容易打桩) 中等(需要追踪订阅关系)
内存开销 小(静态数组) 较大(链表节点动态分配)

4.4 事件路由表设计

路由表是事件分发的核心数据结构。设计得好,系统清晰;设计得烂,调试地狱。

4.4.1 路由表结构

// 我常用的路由表结构
typedef struct {
    uint32_t        event_id;       // 事件ID
    uint8_t         priority;       // 优先级(0最高)
    void           *handler;        // 处理函数指针
    uint32_t        timeout_ms;     // 超时时间(0表示不超时)
    const char     *desc;           // 描述(调试用)
} route_entry_t;

// 路由表实例
static route_entry_t g_route_table[] = {
    {EVT_EMERGENCY_STOP, 0, emergency_handler,   100, "急停事件"},
    {EVT_SENSOR_ALARM,   1, alarm_handler,       500, "传感器告警"},
    {EVT_UI_REFRESH,     2, ui_refresh_handler,    0, "界面刷新"},
    // ...
};

关键设计原则:

  • 事件ID用枚举,不要用魔数
  • 优先级字段用于中断上下文和任务上下文的分流
  • 描述字段在调试时能救命——我曾经靠它定位了一个事件ID冲突的bug

4.4.2 路由查找优化

如果路由表条目超过几十个,线性查找就不太行了。我一般用两种优化:

  • 哈希索引:按事件ID哈希到不同的桶,每个桶内链表较短
  • 二分查找:路由表按事件ID排序,用二分查找定位
// 二分查找路由表
static route_entry_t *route_find(uint32_t event_id) {
    int left = 0, right = ROUTE_TABLE_SIZE - 1;
    while (left <= right) {
        int mid = (left + right) >> 1;
        if (g_route_table[mid].event_id == event_id)
            return &g_route_table[mid];
        else if (g_route_table[mid].event_id < event_id)
            left = mid + 1;
        else
            right = mid - 1;
    }
    return NULL;
}

4.5 动态注册与注销

动态注册和注销,是订阅发布模式的核心能力。但嵌入式系统里做动态操作,要格外小心。

4.5.1 注册流程

  1. 分配订阅节点(从预分配池或堆)
  2. 填充事件ID和处理函数指针
  3. 插入订阅链表(注意临界区保护)
  4. 可选:注册回调通知(比如日志记录)

4.5.2 注销流程

  1. 从链表中找到目标节点
  2. 从链表中摘除(注意中断安全)
  3. 释放节点内存(或归还到池中)
  4. 清理相关资源(如定时器、缓冲区)

我曾经踩过的坑:在中断服务函数里直接调用注销接口,结果链表操作破坏了临界区,导致系统随机崩溃。后来我改用「延迟注销」——在中断里只标记,等任务上下文再真正执行注销。

4.5.3 安全注销示例

// 延迟注销机制
static volatile bool g_unregister_pending = false;
static uint32_t g_pending_event_id;

void event_bus_unregister_safe(uint32_t event_id) {
    // 中断上下文中只标记
    if (in_isr()) {
        g_unregister_pending = true;
        g_pending_event_id = event_id;
        return;
    }
    // 任务上下文中直接执行
    event_bus_unregister(event_id);
}

// 在主循环或空闲任务中处理延迟注销
void event_bus_process_pending(void) {
    if (g_unregister_pending) {
        event_bus_unregister(g_pending_event_id);
        g_unregister_pending = false;
    }
}

4.6 本章知识结构图

事件分发机制知识结构 事件分发机制 中心化分发器 订阅发布模式 静态路由表 单一入口 确定性强 动态注册 解耦广播 灵活扩展 事件路由表设计 哈希索引 二分查找 优先级分流 动态注册与注销

4.7 选型建议

到底用哪种模式?我个人的经验法则:

  • 系统事件类型固定(<20种) → 中心化分发器,简单可靠
  • 模块间解耦需求高 → 订阅发布模式,扩展性好
  • 硬实时要求(中断级响应) → 中心化分发器 + 静态路由表
  • 动态加载模块 → 订阅发布模式 + 延迟注销机制

一个小技巧:我经常在项目初期先用中心化分发器快速验证,等模块多了再切换到订阅发布模式。路由表接口设计得通用一些,切换成本其实很低。