6. 事件驱动架构基础:什么是事件驱动、事件循环、事件处理器、事件分发机制

好,咱们今天聊点真正有意思的东西——事件驱动架构。

我刚开始做嵌入式那会儿,写的代码全是轮询。一个 while 循环里,挨个查按键、查串口、查定时器。代码越长,轮询一圈的时间就越长。按键按下去,可能要等几十毫秒才有反应。你说用户能忍吗?不能。我自己都忍不了。

后来我接触了事件驱动。说白了,就是「有事儿才干活,没事儿就歇着」。这个思路,彻底改变了我写代码的方式。

什么是事件驱动?

事件驱动,核心就一句话:系统的行为由外部或内部的事件来触发

你按下一个按键,系统产生一个「按键事件」。串口收到一个字节,系统产生一个「数据到达事件」。定时器溢出,系统产生一个「超时事件」。每个事件都有对应的处理函数,事件一来,函数就跑。

这和轮询有什么区别?

对比项 轮询模式 事件驱动模式
CPU 利用率 空转也占 CPU 无事可做时休眠
响应延迟 取决于轮询周期 事件到达即处理
代码结构 线性、容易膨胀 模块化、松耦合
扩展性 加功能要改主循环 加事件处理器即可

我在一个智能家居项目里,最初用轮询处理 8 个传感器。后来客户要加到 32 个,轮询一圈的时间从 5ms 涨到了 40ms。有些传感器数据都变了,我还在查别的。换成事件驱动后,每个传感器有变化才上报,CPU 负载反而降了 60%。

事件循环:系统的「心脏」

事件驱动架构里,最核心的组件就是事件循环。它是个死循环,一直在做三件事:

  1. 从事件队列里取出一个事件
  2. 找到这个事件对应的处理器
  3. 调用处理器,执行逻辑

代码写出来,其实特别简单:

while (1) {
    Event evt = event_queue_dequeue();  // 取出事件
    EventHandler handler = find_handler(evt.type); // 找处理器
    if (handler) {
        handler(evt.data);  // 执行处理
    }
}

嗯,就这么几行。但你别小看它。这个循环的调度策略,直接决定了系统的实时性。

关键点:事件循环不能阻塞。如果某个事件处理器执行时间太长,后续事件都得排队等着。我见过一个项目,串口事件处理器里做了个 200ms 的延时,结果按键响应直接卡死。后来改成异步处理,才解决问题。

事件处理器:干活的「工人」

事件处理器,就是真正干活的函数。每个事件类型对应一个处理器。你想想看,这天然就是模块化的——按键的代码不会和串口的代码搅在一起。

我一般这样定义处理器:

typedef void (*EventHandler)(void *data);

typedef struct {
    uint32_t type;
    EventHandler handler;
} EventHandlerEntry;

然后注册的时候,就像填表格一样:

EventHandlerEntry handlers[] = {
    {EVENT_KEY_PRESS,    key_press_handler},
    {EVENT_UART_RX,      uart_rx_handler},
    {EVENT_TIMER_TICK,   timer_tick_handler},
    {EVENT_SENSOR_READY, sensor_ready_handler},
};

每个处理器只做自己分内的事。按键处理器只管解析按键码,串口处理器只管把数据塞进缓冲区。至于数据怎么用?那是别的模块的事。

我的习惯:处理器里不要做复杂计算,不要调用阻塞函数。如果确实需要,就把任务拆成多个事件,分步执行。我曾经在一个处理器里做了浮点运算,结果把整个事件循环拖慢了 3ms。后来改成预计算查表,问题就解决了。

事件分发机制:谁来找「工人」?

事件分发,就是事件循环里那个 find_handler 函数干的事。它负责把事件和处理器匹配起来。

常见的分发方式有三种:

  • 查表法:用事件类型做索引,直接查处理器数组。速度最快,O(1) 复杂度。适合事件类型固定的场景。
  • 哈希法:事件类型通过哈希映射到处理器。灵活,但可能有冲突。适合事件类型很多的情况。
  • 链式匹配:遍历一个链表,逐个比对事件类型。最灵活,但速度慢。适合动态注册/注销处理器的系统。

我大部分项目都用查表法。简单、可靠、容易调试。你想想看,嵌入式系统里事件类型一般不超过 64 个,一张表就能搞定,何必搞复杂?

下面这张图,把整个流程串起来了:

事件驱动架构核心流程 事件源 按键 / 串口 / 定时器 事件队列 FIFO 缓冲区 事件循环 while(1) 调度 分发器 事件处理器表 EVENT_KEY_PRESS key_handler EVENT_UART_RX uart_handler EVENT_TIMER timer_handler 处理器执行 处理完成,继续取下一个事件 事件源 队列 循环 分发 处理

事件队列:缓冲与解耦

事件队列的存在,让事件产生和处理在时间上解耦了。中断里产生的事件,可以快速入队,然后立刻退出中断。事件循环在后台慢慢处理。

队列的设计有几个要点:

  • 大小要合理:太小会丢事件,太大浪费内存。我一般按最坏情况估算,再留 20% 余量。
  • 入队要快:中断里入队,绝对不能阻塞。用无锁环形队列是最常见的做法。
  • 优先级要考虑:有些事件需要紧急处理,比如看门狗喂狗事件。可以用多级队列,高优先级先出队。

注意:我曾经在一个项目里,把事件队列设成了 16 个。结果某个外设疯狂产生事件,队列满了,新事件直接丢弃。按键按下去没反应,用户以为是死机了。后来我把队列改成 64 个,并且加了事件去重逻辑,才彻底解决。

事件驱动的好处,不只是省电

很多人觉得事件驱动就是为了省电。其实不止。它最大的好处是让代码结构变得清晰

你想想看,每个模块只需要关心自己产生什么事件、处理什么事件。模块之间不需要互相知道对方的存在。这就是松耦合。松耦合的代码,改起来才敢动手。

我维护过一个 5 万行的嵌入式项目,全是轮询。加一个功能,要改 6 个文件。后来重构为事件驱动,每个模块独立编译测试。再改功能,只改一个文件就够了。

嗯,这就是事件驱动的魅力。它不复杂,但用好了,能让你的代码脱胎换骨。


公众号:蓝海资料掘金营,微信 deep3321