抢占式调度与时间片轮转:调度策略对比、优先级反转问题、如何避免死锁
各位做嵌入式开发的同行,今天我们来聊聊 ThreadX 的调度策略。这个话题我琢磨了很久,因为在实际项目中,调度策略选错了,系统响应就会出问题。说白了,调度就是决定“下一个该谁跑”。
抢占式调度 vs 时间片轮转
ThreadX 默认是抢占式调度。什么意思呢?就是高优先级的任务一旦就绪,立马把 CPU 抢过来。我刚开始用 ThreadX 时,觉得这机制很直接,直到有一次在电机控制项目里踩了坑。
先看抢占式的核心逻辑:
/* 任务A:优先级 5,控制电机 */
void task_A(ULONG thread_input) {
while(1) {
/* 正在执行电机控制算法 */
tx_thread_sleep(10); /* 主动让出CPU */
}
}
/* 任务B:优先级 3,处理紧急报警 */
void task_B(ULONG thread_input) {
while(1) {
/* 检测到过流,需要立即响应 */
/* 抢占 task_A,获得 CPU */
tx_thread_sleep(100);
}
}
抢占式的好处很明显:紧急任务能立刻响应。但有个问题——如果高优先级任务一直不释放 CPU,低优先级任务就永远没机会跑。这就是“饿死”现象。
时间片轮转就不一样了。每个任务分到固定的时间片,轮流执行。ThreadX 里用 tx_thread_time_slice 来设置:
/* 创建任务时指定时间片 */
tx_thread_create(&thread_A, "Task A",
task_A, 0, pointer_A, DEMO_STACK_SIZE,
5, 5, /* 优先级5,时间片5个tick */
TX_NO_TIME_SLICE, TX_AUTO_START);
/* 或者运行时修改 */
tx_thread_time_slice_change(&thread_A, 10);
我个人习惯在交互类任务上用时间片轮转。比如按键扫描和 LCD 刷新,优先级相同,轮流跑,谁也不耽误谁。
- 抢占式:高优先级随时打断低优先级,适合实时性要求高的场景
- 时间片轮转:同优先级任务公平分享 CPU,适合交互类、非紧急任务
你想想看,如果所有任务都用抢占式,优先级设置不合理,系统就会乱套。我见过一个项目,把所有任务优先级都设成 0(最高),结果抢占式调度直接退化成轮转,实时性反而下降了。
优先级反转问题
这是 RTOS 里最经典的坑。什么叫优先级反转?简单说就是:高优先级任务在等低优先级任务释放资源,而低优先级任务又被中等优先级任务抢占了。
我画个图你就明白了:
我在一个数据采集项目里遇到过这个问题。任务 H 负责处理高速采样数据,任务 L 负责把数据写入 SD 卡。H 要等 L 写完才能继续,结果中间插进来一个任务 M 做界面刷新。H 被活活拖了 200ms,采样数据全丢了。
ThreadX 提供了两种机制来解决:
| 机制 | 原理 | 适用场景 |
|---|---|---|
| 优先级继承 | 低优先级任务临时提升到高优先级 | 资源竞争不频繁的场景 |
| 优先级天花板 | 访问资源时直接提到最高可能优先级 | 资源竞争频繁、需要确定性 |
如何避免死锁
死锁是调度策略的终极杀手。四个必要条件:互斥、持有并等待、不可剥夺、循环等待。缺一个都不会死锁。
ThreadX 里最常见的死锁场景是这样的:
/* 任务1:先拿锁A,再拿锁B */
void task_1(ULONG input) {
tx_mutex_get(&mutex_A, TX_WAIT_FOREVER);
/* 做一些操作 */
tx_mutex_get(&mutex_B, TX_WAIT_FOREVER);
/* 临界区 */
tx_mutex_put(&mutex_B);
tx_mutex_put(&mutex_A);
}
/* 任务2:先拿锁B,再拿锁A */
void task_2(ULONG input) {
tx_mutex_get(&mutex_B, TX_WAIT_FOREVER);
/* 做一些操作 */
tx_mutex_get(&mutex_A, TX_WAIT_FOREVER);
/* 临界区 */
tx_mutex_put(&mutex_A);
tx_mutex_put(&mutex_B);
}
两个任务各拿一把锁,都在等对方释放。死锁了。
我曾经在一个多传感器融合项目里遇到这个问题。三个任务共享两个 I2C 总线,锁的顺序没统一,结果系统跑着跑着就卡死了。排查了整整两天。
避免死锁的实用方法:
- 固定锁顺序:所有任务按相同顺序获取锁,这是最简单有效的方法
- 使用超时:
tx_mutex_get不要用TX_WAIT_FOREVER,给个超时时间 - 减少锁的粒度:能用一个锁就别用两个,锁越少死锁概率越低
- 避免嵌套锁:在持有锁的情况下不要再拿其他锁
ThreadX 还提供了 tx_mutex_get 的 TX_NO_INHERIT 选项。如果你确定不会发生优先级反转,可以用这个选项减少开销。但我不建议新手这么干,容易出问题。
- 抢占式调度:紧急任务用高优先级,但要防止饿死
- 时间片轮转:同优先级任务公平分享,适合非实时场景
- 优先级反转:用优先级继承解决,别省这个开销
- 死锁:固定锁顺序 + 超时机制,双保险
嗯,调度策略这块就聊到这。记住一点:没有万能的调度策略,只有适合你项目的调度策略。选对了,系统稳如老狗;选错了,调试到怀疑人生。