2. 核心概念与术语:事件、事件源、事件处理器、事件队列、事件循环

好,我们正式开始。这一章要聊的是事件驱动架构的“五根柱子”——五个最基础的概念。你可能会觉得这些词听起来很简单,但说实话,我在带团队的时候发现,很多老手都会在这些基础概念上栽跟头。不是不懂,而是理解得不够深。

咱们一个一个来拆。

2.1 事件(Event)—— 系统里发生的“一件事”

事件是什么?说白了,就是“发生了某件事”这个信息本身。它不是一个函数调用,也不是一个中断信号,而是一个数据对象。

我习惯把事件看作一个“信封”。信封外面写着事件类型(比如“按键按下”),里面装着具体数据(比如“哪个按键、按了多久”)。

核心要点:事件是不可变的。一旦创建,就不应该被修改。你只能读取它,或者把它扔掉。

一个典型的事件结构长这样:

// 事件类型枚举
typedef enum {
    EVT_NONE = 0,
    EVT_BUTTON_PRESSED,
    EVT_TIMER_EXPIRED,
    EVT_SENSOR_DATA_READY,
    EVT_UART_RX_COMPLETE,
    EVT_SYSTEM_ERROR
} EventType_t;

// 事件结构体
typedef struct {
    EventType_t type;      // 事件类型
    uint32_t    timestamp; // 事件发生的时间戳
    uint32_t    data;      // 携带的数据(或指针)
    uint8_t     priority;  // 优先级
} Event_t;

嗯,这里要注意:timestamp 字段我强烈建议加上。我在项目中遇到过好几次,事件在队列里排队排了半天,处理的时候已经不知道它是什么时候发生的了。有了时间戳,你至少能判断这个事件是否已经“过期”。

2.2 事件源(Event Source)—— 谁产生了这个事件?

事件源就是事件的“生产者”。它可以是硬件,也可以是软件。

  • 硬件事件源:GPIO 中断、定时器溢出、UART 接收完成、ADC 转换结束……
  • 软件事件源:用户代码主动调用 postEvent()、协议栈解析完成、任务超时……

你想想看,一个系统里可能同时有十几个事件源在“生产”事件。如果它们各自为政,系统就乱套了。所以我们需要一个统一的“入口”——事件队列。

个人经验:我建议每个事件源在发布事件时,把自己的 ID 也塞进事件结构里。这样调试的时候,你能一眼看出“这个事件是谁发的”。我曾经因为没加这个字段,排查一个随机崩溃问题花了整整两天。

2.3 事件处理器(Event Handler)—— 真正干活的家伙

事件处理器就是“消费者”。它负责响应某个事件,执行对应的逻辑。

每个事件处理器通常只处理一种或一类事件。比如:

  • buttonHandler 只处理按键事件
  • timerHandler 只处理定时器事件
  • errorHandler 只处理错误事件

处理器的签名一般长这样:

typedef void (*EventHandler_t)(const Event_t *event);

void buttonHandler(const Event_t *event) {
    // 只处理按键事件
    if (event->type != EVT_BUTTON_PRESSED) return;
    
    uint8_t key = (uint8_t)(event->data & 0xFF);
    // 处理按键逻辑...
}

避坑指南:我曾经见过有人在一个处理器里处理所有事件——用 switch-case 写了上百行。千万别这么干。每个处理器只做一件事,这是事件驱动架构的黄金法则。否则你的代码会变成一坨“意大利面条”。

2.4 事件队列(Event Queue)—— 事件的“缓冲区”

事件队列是连接生产者和消费者的桥梁。它是一个先进先出(FIFO)的缓冲区。

为什么需要队列?因为事件源产生事件的速度和事件处理器处理事件的速度往往不一致。没有队列,事件就会丢失。

一个简单的队列实现:

#define EVENT_QUEUE_SIZE 32

typedef struct {
    Event_t buffer[EVENT_QUEUE_SIZE];
    uint8_t head;
    uint8_t tail;
    uint8_t count;
} EventQueue_t;

bool enqueue(EventQueue_t *q, const Event_t *event) {
    if (q->count >= EVENT_QUEUE_SIZE) return false; // 队列满
    q->buffer[q->head] = *event;
    q->head = (q->head + 1) % EVENT_QUEUE_SIZE;
    q->count++;
    return true;
}

bool dequeue(EventQueue_t *q, Event_t *event) {
    if (q->count == 0) return false; // 队列空
    *event = q->buffer[q->tail];
    q->tail = (q->tail + 1) % EVENT_QUEUE_SIZE;
    q->count--;
    return true;
}

嗯,这里要注意队列大小的选择。太小了容易丢事件,太大了浪费内存。我一般按系统里最坏情况下可能积压的事件数乘以 1.5 来估算。

2.5 事件循环(Event Loop)—— 系统的“心脏”

事件循环是一个永不停止的 while 循环。它做的事情很简单:

  1. 从队列里取出一个事件
  2. 找到对应的事件处理器
  3. 调用处理器
  4. 回到步骤 1

代码看起来就是这样:

void eventLoop(void) {
    Event_t event;
    while (1) {
        if (dequeue(&g_eventQueue, &event)) {
            EventHandler_t handler = findHandler(event.type);
            if (handler) {
                handler(&event);
            }
        }
        // 如果没有事件,可以进入低功耗模式
        else {
            enterSleepMode();
        }
    }
}

这就是整个系统的“心脏”。只要它还在跳,系统就在工作。

2.6 它们之间的关系

这五个概念的关系,我用一张图来总结:

事件驱动核心概念关系图 事件源 (生产者) 事件 (数据对象) 事件队列 (FIFO缓冲区) 事件循环 (调度核心) 事件处理器 (消费者) 产生 入队 出队 分发 数据流方向:事件源 → 事件 → 事件队列 → 事件循环 → 事件处理器 整个系统围绕“事件”这个核心数据对象运转

2.7 一张表总结

概念 角色 核心职责 常见误区
事件 数据对象 携带“发生了什么”的信息 把事件和中断处理混为一谈
事件源 生产者 产生并发布事件 在中断里直接处理逻辑,而不是发布事件
事件处理器 消费者 响应事件,执行业务逻辑 一个处理器处理所有事件
事件队列 缓冲区 解耦生产者和消费者 队列大小设置不合理导致丢事件
事件循环 调度器 从队列取事件并分发 在循环里做耗时操作阻塞系统

我的建议:刚开始学事件驱动架构时,先把这五个概念刻在脑子里。每次写代码前,问自己三个问题:

  1. 这个事件是谁产生的?
  2. 它要经过队列吗?
  3. 谁来处理它?

想清楚这三个问题,你的架构就不会乱。