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