6、结构体封装实战:任务调度器状态机、状态机上下文管理、代码实现

好,咱们今天聊点实在的。

前面几章我们把状态机的理论、表格驱动、函数指针都过了一遍。说实话,那些都是基本功。但真正到了项目里,你会发现一个尴尬的问题——状态机跑起来没问题,可一旦要管理多个状态机实例,代码就开始变得臃肿、混乱。

为什么会这样?

因为你没有用结构体把状态机的上下文封装起来。

6.1 为什么需要上下文管理?

我先讲个我自己的经历。几年前做一个物联网网关项目,需要同时管理十几个设备的状态机。每个设备都有独立的运行状态、超时计时器、重试次数、数据缓冲区。一开始我图省事,全用全局变量。

结果呢?

代码写到一半,我自己都分不清哪个变量属于哪个设备了。调试的时候,改一个全局变量,莫名其妙影响了另一个设备的状态。那段时间我几乎每天都在跟bug搏斗,后来痛定思痛,决定用结构体把状态机的上下文封装起来。

说白了,状态机上下文就是状态机运行过程中需要记住的所有数据。包括当前状态、事件队列、计时器、用户数据等等。把这些东西塞进一个结构体里,每个状态机实例都有自己的上下文,互不干扰。

核心原则:一个状态机实例 = 一个上下文结构体。实例之间完全隔离。

6.2 任务调度器状态机的设计

咱们今天用一个实际例子来讲解——任务调度器状态机。

这个调度器管理多个任务的执行。每个任务有四个状态:空闲就绪运行挂起。任务在状态之间切换,调度器负责决定哪个任务该运行。

先看状态转换图:

任务调度器状态机 空闲 就绪 运行 挂起 创建任务 调度执行 时间片用完 等待事件 事件到达 删除任务

嗯,这张图很直观。但真正实现的时候,你会发现每个任务都需要记录自己的状态、优先级、剩余时间片、等待的事件类型等等。这些数据,就是任务的上下文。

6.3 结构体封装实战

咱们直接上代码。我习惯把状态机的上下文定义成一个结构体,里面包含状态变量和所有需要的数据。

/* task_scheduler.h */
#ifndef TASK_SCHEDULER_H
#define TASK_SCHEDULER_H

#include <stdint.h>

/* 任务状态枚举 */
typedef enum {
    TASK_IDLE = 0,
    TASK_READY,
    TASK_RUNNING,
    TASK_SUSPENDED
} task_state_t;

/* 事件类型 */
typedef enum {
    EVT_NONE = 0,
    EVT_CREATE,
    EVT_DELETE,
    EVT_SCHEDULE,
    EVT_TIMEOUT,
    EVT_WAKEUP
} event_type_t;

/* 事件结构体 */
typedef struct {
    event_type_t type;
    uint32_t param;      /* 事件参数,比如超时值 */
} event_t;

/* 任务上下文结构体 —— 这就是核心 */
typedef struct {
    /* 状态机核心 */
    task_state_t state;          /* 当前状态 */
    event_t      pending_event;  /* 待处理事件 */
    
    /* 任务属性 */
    uint32_t     task_id;        /* 任务ID */
    uint8_t      priority;       /* 优先级 0-255 */
    uint32_t     time_slice;     /* 时间片(毫秒) */
    uint32_t     remaining_time; /* 剩余时间 */
    
    /* 挂起相关 */
    event_type_t wait_event;     /* 等待的事件类型 */
    uint32_t     suspend_timer;  /* 挂起计时器 */
    
    /* 用户数据指针 */
    void        *user_data;      /* 指向任务私有数据 */
    
    /* 统计信息 */
    uint32_t     run_count;      /* 运行次数 */
    uint32_t     total_time;     /* 总运行时间 */
} task_context_t;

/* 状态机处理函数 */
void task_scheduler_init(task_context_t *ctx, uint32_t task_id, uint8_t priority);
void task_scheduler_process(task_context_t *ctx, event_t *evt);

#endif /* TASK_SCHEDULER_H */

你看,这个结构体把任务的所有信息都包进去了。每个任务只需要一个 task_context_t 实例,调度器通过操作这个结构体来管理任务。

我的习惯:结构体里的成员按功能分组,用注释标明。这样别人看代码的时候,一眼就能知道哪些是状态机核心,哪些是任务属性,哪些是统计信息。

6.4 状态机处理函数的实现

接下来是状态机处理函数。这里我用的是经典的 switch-case 结构,但注意——每个 case 分支里,我们操作的都是传入的上下文指针。

/* task_scheduler.c */
#include "task_scheduler.h"

void task_scheduler_init(task_context_t *ctx, uint32_t task_id, uint8_t priority)
{
    ctx->state          = TASK_IDLE;
    ctx->pending_event.type = EVT_NONE;
    ctx->task_id        = task_id;
    ctx->priority       = priority;
    ctx->time_slice     = 100;   /* 默认100ms */
    ctx->remaining_time = 0;
    ctx->wait_event     = EVT_NONE;
    ctx->suspend_timer  = 0;
    ctx->user_data      = NULL;
    ctx->run_count      = 0;
    ctx->total_time     = 0;
}

void task_scheduler_process(task_context_t *ctx, event_t *evt)
{
    /* 保存事件 */
    ctx->pending_event = *evt;
    
    switch (ctx->state) {
        case TASK_IDLE:
            if (evt->type == EVT_CREATE) {
                ctx->state = TASK_READY;
                /* 创建任务成功 */
            }
            break;
            
        case TASK_READY:
            if (evt->type == EVT_SCHEDULE) {
                ctx->state = TASK_RUNNING;
                ctx->remaining_time = ctx->time_slice;
                ctx->run_count++;
                /* 开始执行任务 */
            } else if (evt->type == EVT_DELETE) {
                ctx->state = TASK_IDLE;
                /* 任务被删除 */
            }
            break;
            
        case TASK_RUNNING:
            if (evt->type == EVT_TIMEOUT) {
                ctx->state = TASK_READY;
                ctx->total_time += ctx->time_slice;
                /* 时间片用完,让出CPU */
            } else if (evt->type == EVT_WAKEUP && 
                       evt->param == ctx->wait_event) {
                ctx->state = TASK_SUSPENDED;
                ctx->suspend_timer = 0;
                /* 任务等待事件,进入挂起 */
            }
            break;
            
        case TASK_SUSPENDED:
            if (evt->type == EVT_WAKEUP && 
                evt->param == ctx->wait_event) {
                ctx->state = TASK_READY;
                /* 等待的事件到达,回到就绪 */
            }
            break;
            
        default:
            /* 异常状态处理 */
            ctx->state = TASK_IDLE;
            break;
    }
}

这段代码看起来简单,但有几个细节我特别想强调:

  • 事件先保存再处理:万一处理过程中需要重新调度,事件不会丢失。
  • 每个状态只处理自己关心的事件:不相关的事件直接忽略,不会导致状态错乱。
  • default 分支做异常恢复:我曾经遇到过状态变量被意外篡改的情况,default 分支把状态拉回空闲,至少系统不会崩溃。

6.5 多实例管理

好了,现在我们有上下文结构体了,怎么管理多个任务呢?

很简单,定义一个数组:

#define MAX_TASKS 16

static task_context_t task_pool[MAX_TASKS];

/* 初始化所有任务 */
void scheduler_init_all(void)
{
    for (int i = 0; i < MAX_TASKS; i++) {
        task_scheduler_init(&task_pool[i], i, 0);
    }
}

/* 调度主循环 */
void scheduler_run(void)
{
    event_t evt;
    
    while (1) {
        /* 遍历所有任务 */
        for (int i = 0; i < MAX_TASKS; i++) {
            /* 从队列获取事件(简化处理) */
            evt.type = EVT_SCHEDULE;
            evt.param = 0;
            
            /* 处理任务状态机 */
            task_scheduler_process(&task_pool[i], &evt);
        }
        
        /* 延时等待(实际项目中用定时器中断) */
        delay_ms(10);
    }
}

你看,每个任务都有自己的 task_context_t,调度器通过数组下标访问。互不干扰,清晰明了。

我曾经踩过的坑:一开始我把上下文结构体定义得太大,里面塞了各种缓冲区。结果任务数量一多,内存直接爆了。后来我改成动态分配,或者把大缓冲区放到外部,上下文里只放指针。记住——上下文结构体要尽量精简,只放状态机运行必需的数据。

6.6 上下文管理的进阶技巧

在实际项目中,我还会在上下文结构体里加一些辅助字段,方便调试和扩展:

字段 类型 用途
state_name const char* 当前状态名称,调试时直接打印
last_event event_t 上一次处理的事件,用于回溯
error_code uint32_t 错误码,状态异常时记录
flags uint8_t 标志位,比如是否启用、是否暂停

这些字段不参与状态机逻辑,但调试的时候特别有用。我习惯在产品开发阶段保留这些字段,等量产的时候再通过宏定义去掉,节省内存。

6.7 代码实现总结

好,咱们把今天的内容捋一捋:

  • 为什么需要上下文管理——多个状态机实例共存时,全局变量会互相干扰。结构体封装是唯一的出路。
  • 任务调度器状态机——四个状态:空闲、就绪、运行、挂起。每个状态只处理特定事件。
  • 结构体封装实战——task_context_t 包含状态、事件、属性、统计信息。每个任务一个实例。
  • 多实例管理——用数组或链表管理多个上下文,调度器遍历处理。
  • 进阶技巧——加调试字段、精简结构体、动态分配内存。

说实话,状态机本身不难,难的是怎么把代码组织得干净、可维护。结构体封装就是解决这个问题的钥匙。你想想看,如果每个状态机实例都带着自己的上下文,代码的复用性、可读性都会提升一大截。

嗯,今天就到这里。下一章咱们聊聊状态机的代码生成工具,我会分享一个我自己写的脚本,能自动生成状态机框架代码。到时候你就能体会到,什么叫「写一次,生成无数次」。

核心要点回顾:

  • 上下文结构体是状态机实例的「身份证」
  • 每个实例独立运行,互不干扰
  • 结构体要精简,只放必要数据
  • 调试字段在量产时去掉

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