4. 事件驱动状态机框架设计:事件队列与分发机制

好,咱们进入第四章。这一章我打算把事件驱动状态机框架里最核心的几个零件拆开来讲——事件队列、事件分发、还有那个永不停歇的状态机引擎循环。

说实话,这部分我踩过的坑最多。早期我设计过一个智能家居网关,事件队列用的动态链表,结果跑着跑着内存碎片化严重,最后系统直接挂掉。从那以后,我对队列实现的选择就变得非常谨慎。

4.1 事件队列的设计与实现

事件队列,说白了就是事件的“缓冲区”。生产者(中断、定时器、其他任务)往里扔事件,消费者(状态机引擎)往外取事件。队列设计的好坏,直接决定了系统的实时性和稳定性。

4.1.1 环形缓冲区(Ring Buffer)

我个人最偏爱的方式——环形缓冲区。它固定大小、无动态内存分配、O(1) 的入队出队,非常适合资源受限的嵌入式系统。

核心思路: 用数组 + 头尾指针实现循环。头指针指向下一个要读取的位置,尾指针指向下一个要写入的位置。
/* 环形缓冲区结构体 */
typedef struct {
    Event_t buffer[EVENT_QUEUE_SIZE];  /* 固定数组 */
    uint32_t head;                     /* 读指针 */
    uint32_t tail;                     /* 写指针 */
    uint32_t count;                    /* 当前元素数量 */
} RingBuffer_t;

/* 入队操作 */
bool RingBuffer_Push(RingBuffer_t *q, Event_t *evt) {
    if (q->count >= EVENT_QUEUE_SIZE) {
        return false;  /* 队列满,丢弃事件 */
    }
    q->buffer[q->tail] = *evt;
    q->tail = (q->tail + 1) % EVENT_QUEUE_SIZE;
    q->count++;
    return true;
}

/* 出队操作 */
bool RingBuffer_Pop(RingBuffer_t *q, Event_t *evt) {
    if (q->count == 0) {
        return false;  /* 队列空 */
    }
    *evt = q->buffer[q->head];
    q->head = (q->head + 1) % EVENT_QUEUE_SIZE;
    q->count--;
    return true;
}
我的经验: 队列大小一定要根据最坏情况来定。我曾经在一个 CAN 总线项目中,把队列设成 16,结果高负载时连续丢事件。后来改成 64,问题解决。建议用 2 的幂次 作为大小,这样取模可以用位运算加速。

4.1.2 链表队列(Linked List Queue)

环形缓冲区虽好,但有个硬伤——大小固定。如果事件数量波动很大,或者你无法预估峰值,链表队列就派上用场了。

链表队列的每个节点动态分配,理论上只受堆空间限制。但代价是什么?内存碎片、分配耗时、还有指针操作的开销。

/* 链表节点 */
typedef struct EventNode {
    Event_t event;
    struct EventNode *next;
} EventNode_t;

/* 链表队列 */
typedef struct {
    EventNode_t *head;   /* 出队端 */
    EventNode_t *tail;   /* 入队端 */
    uint32_t count;
} LinkedListQueue_t;

/* 入队 */
bool LinkedListQueue_Push(LinkedListQueue_t *q, Event_t *evt) {
    EventNode_t *node = (EventNode_t *)malloc(sizeof(EventNode_t));
    if (!node) return false;
    node->event = *evt;
    node->next = NULL;
    if (q->tail) {
        q->tail->next = node;
    } else {
        q->head = node;
    }
    q->tail = node;
    q->count++;
    return true;
}
避坑指南: 我曾经在一个长期运行的设备上用链表队列,跑了三个月后系统变慢。查了半天,发现是频繁 malloc/free 导致堆碎片化,分配越来越慢。后来我改用内存池 + 固定节点数量的链表,才彻底解决。如果你用链表队列,一定要配合内存池。

4.1.3 两种队列的对比

特性 环形缓冲区 链表队列
内存分配 静态,编译时确定 动态,运行时分配
大小限制 固定,不可扩展 理论上无限(受堆限制)
入队/出队速度 O(1),极快 O(1),但 malloc 有开销
内存碎片 无 有,长期运行需注意
适用场景 事件速率稳定、资源受限 事件波动大、需要弹性

4.2 事件分发机制

队列里存了一堆事件,怎么发给对应的状态机?这就是分发机制要干的事。

4.2.1 直接分发(Direct Dispatch)

最简单的做法——从队列取出事件,直接调用目标状态机的处理函数。每个事件都带一个 target 字段,指明发给谁。

void Dispatcher_Direct(Event_t *evt) {
    StateMachine_t *sm = evt->target_sm;
    if (sm && sm->handler) {
        sm->handler(sm, evt);  /* 直接调用 */
    }
}

直接分发的优点是延迟极低,没有中间环节。但缺点也很明显——如果某个状态机处理时间过长,会阻塞后续所有事件的分发。

4.2.2 优先级分发(Priority Dispatch)

现实世界的事件是有轻重缓急的。比如按键事件可以等一等,但紧急停止按钮的事件必须立刻处理。这时候就需要优先级分发。

实现方式有两种:

  • 多队列方案: 每个优先级一个队列,分发时从高优先级队列开始轮询。
  • 单队列 + 优先级排序: 所有事件进一个队列,但按优先级排序,高优先级事件插队到前面。
我推荐多队列方案: 实现简单,入队 O(1),出队时只需按优先级顺序检查各队列。单队列排序在事件多时性能会急剧下降。
/* 多优先级队列 */
#define PRIORITY_LEVELS 3
RingBuffer_t priorityQueues[PRIORITY_LEVELS];

/* 分发循环 */
void Dispatcher_Priority(void) {
    for (int pri = PRIORITY_LEVELS - 1; pri >= 0; pri--) {
        Event_t evt;
        while (RingBuffer_Pop(&priorityQueues[pri], &evt)) {
            StateMachine_t *sm = evt.target_sm;
            if (sm && sm->handler) {
                sm->handler(sm, &evt);
            }
        }
    }
}
注意: 优先级反转问题在这里同样存在。如果一个低优先级事件的处理函数中产生了高优先级事件,而高优先级事件又依赖低优先级状态机的状态...嗯,你想想看,是不是死锁了?我建议在事件处理中不要嵌套产生新事件,或者用独立的事件队列来规避。

4.3 状态机引擎核心循环

好了,队列有了,分发机制有了,现在把它们串起来——这就是状态机引擎的核心循环。

核心循环的逻辑其实很简单:

  1. 从事件队列取出一个事件
  2. 根据事件的目标状态机,调用对应的状态处理函数
  3. 状态处理函数执行状态转移逻辑
  4. 回到步骤 1,继续取下一个事件
void StateMachineEngine_Run(void) {
    while (1) {  /* 永不停止的主循环 */
        Event_t evt;
        
        /* 1. 从队列取事件 */
        if (!RingBuffer_Pop(&g_eventQueue, &evt)) {
            /* 队列空,可以进入低功耗模式或执行空闲任务 */
            IdleHook();
            continue;
        }
        
        /* 2. 分发事件 */
        StateMachine_t *sm = evt.target_sm;
        if (sm && sm->handler) {
            sm->handler(sm, &evt);
        }
        
        /* 3. 检查是否有定时器超时事件需要插入 */
        Timer_CheckAndPost();
    }
}

这个循环看起来简单,但实际工程中要考虑几个关键点:

  • 空闲处理: 队列空时别空转浪费 CPU,可以进入睡眠模式,或者执行一些低优先级的后台任务。
  • 看门狗喂食: 如果某个状态处理函数执行时间过长,看门狗可能会超时复位。我习惯在循环顶部喂一次狗。
  • 事件超时: 有些事件有时效性,比如 100ms 内必须处理,否则就失效。可以在出队时检查时间戳。
我曾经踩过的坑: 在一个电机控制项目中,核心循环里直接调用了状态处理函数,而某个状态处理中有一个阻塞式的传感器读取操作,耗时 50ms。结果所有事件都被堵住了,系统响应变得极其迟钝。后来我把阻塞操作改成异步 + 状态机内部状态切换,才解决问题。记住:状态处理函数里不要做阻塞操作。

4.4 整体框架图

下面这张图把事件队列、分发机制和引擎循环的关系画出来了。你看一眼就能明白整个框架的脉络。

事件驱动状态机框架核心结构 事件源 中断 定时器 其他任务 产生事件 事件队列 环形缓冲区 链表队列 多优先级队列 FIFO / 优先级 取出事件 事件分发器 直接分发 优先级分发 广播分发 按目标/优先级 调用 状态机 引擎核心循环:取事件 → 分发 → 处理 → 空闲 → 重复 循环反馈 关键设计要点: • 队列选择:资源受限用环形缓冲区,事件波动大用链表队列+内存池 • 分发策略:实时性要求高用优先级分发,简单场景用直接分发 • 引擎循环:队列空时执行空闲任务或进入低功耗,不要空转 • 状态处理:禁止阻塞操作,防止事件队列被堵死