6. 事件驱动架构基础:什么是事件驱动、事件循环、事件处理器、事件分发机制
好,咱们今天聊点真正有意思的东西——事件驱动架构。
我刚开始做嵌入式那会儿,写的代码全是轮询。一个 while 循环里,挨个查按键、查串口、查定时器。代码越长,轮询一圈的时间就越长。按键按下去,可能要等几十毫秒才有反应。你说用户能忍吗?不能。我自己都忍不了。
后来我接触了事件驱动。说白了,就是「有事儿才干活,没事儿就歇着」。这个思路,彻底改变了我写代码的方式。
什么是事件驱动?
事件驱动,核心就一句话:系统的行为由外部或内部的事件来触发。
你按下一个按键,系统产生一个「按键事件」。串口收到一个字节,系统产生一个「数据到达事件」。定时器溢出,系统产生一个「超时事件」。每个事件都有对应的处理函数,事件一来,函数就跑。
这和轮询有什么区别?
| 对比项 | 轮询模式 | 事件驱动模式 |
|---|---|---|
| CPU 利用率 | 空转也占 CPU | 无事可做时休眠 |
| 响应延迟 | 取决于轮询周期 | 事件到达即处理 |
| 代码结构 | 线性、容易膨胀 | 模块化、松耦合 |
| 扩展性 | 加功能要改主循环 | 加事件处理器即可 |
我在一个智能家居项目里,最初用轮询处理 8 个传感器。后来客户要加到 32 个,轮询一圈的时间从 5ms 涨到了 40ms。有些传感器数据都变了,我还在查别的。换成事件驱动后,每个传感器有变化才上报,CPU 负载反而降了 60%。
事件循环:系统的「心脏」
事件驱动架构里,最核心的组件就是事件循环。它是个死循环,一直在做三件事:
- 从事件队列里取出一个事件
- 找到这个事件对应的处理器
- 调用处理器,执行逻辑
代码写出来,其实特别简单:
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 个,一张表就能搞定,何必搞复杂?
下面这张图,把整个流程串起来了:
事件队列:缓冲与解耦
事件队列的存在,让事件产生和处理在时间上解耦了。中断里产生的事件,可以快速入队,然后立刻退出中断。事件循环在后台慢慢处理。
队列的设计有几个要点:
- 大小要合理:太小会丢事件,太大浪费内存。我一般按最坏情况估算,再留 20% 余量。
- 入队要快:中断里入队,绝对不能阻塞。用无锁环形队列是最常见的做法。
- 优先级要考虑:有些事件需要紧急处理,比如看门狗喂狗事件。可以用多级队列,高优先级先出队。
注意:我曾经在一个项目里,把事件队列设成了 16 个。结果某个外设疯狂产生事件,队列满了,新事件直接丢弃。按键按下去没反应,用户以为是死机了。后来我把队列改成 64 个,并且加了事件去重逻辑,才彻底解决。
事件驱动的好处,不只是省电
很多人觉得事件驱动就是为了省电。其实不止。它最大的好处是让代码结构变得清晰。
你想想看,每个模块只需要关心自己产生什么事件、处理什么事件。模块之间不需要互相知道对方的存在。这就是松耦合。松耦合的代码,改起来才敢动手。
我维护过一个 5 万行的嵌入式项目,全是轮询。加一个功能,要改 6 个文件。后来重构为事件驱动,每个模块独立编译测试。再改功能,只改一个文件就够了。
嗯,这就是事件驱动的魅力。它不复杂,但用好了,能让你的代码脱胎换骨。
公众号:蓝海资料掘金营,微信 deep3321