4. 线程管理与调度
调度策略,说白了就是操作系统决定「下一个该谁跑」的规则。ThreadX 默认是抢占式调度,但实际项目中很少只用一种策略。我做过一个工业控制项目,电机控制和通信任务混在一起,光靠默认调度根本跑不顺。今天咱们就把抢占式、时间片轮转、优先级反转这几个核心问题掰开揉碎讲清楚。
抢占式调度原理
抢占式调度,核心就一句话:高优先级任务就绪时,立即打断当前任务。ThreadX 的调度器在以下时刻会检查是否需要抢占:
- 任务调用
tx_thread_sleep()主动挂起 - 任务释放信号量、队列等同步对象
- 外部中断触发,ISR 中调用了唤醒任务的 API
- 时间片到期(如果启用了时间片轮转)
我刚开始用 ThreadX 时犯过一个低级错误:在 ISR 里调了 tx_thread_resume(),但忘了检查返回值。结果高优先级任务没被唤醒,低优先级任务一直跑,整个系统响应慢了 200ms。后来我养成了习惯——每次调用调度相关 API,都检查返回值。
- 优先级数字越小,优先级越高(ThreadX 默认 0 为最高)
- 抢占只发生在安全点(系统调用、中断返回时)
- 同优先级任务默认按 FIFO 顺序执行
时间片轮转
同优先级任务如果一直占着 CPU 不放,其他同优先级任务就永远没机会跑。时间片轮转就是解决这个问题的——每个任务跑满一个时间片后,强制切换到下一个同优先级任务。
ThreadX 启用时间片轮转很简单,在创建任务时设置 time_slice 参数:
// 创建两个同优先级任务,各分配 5 个 tick 的时间片
tx_thread_create(&task1, "Task1", task1_entry, 0,
stack1, STACK_SIZE,
10, 10, // 优先级 10,时间片 5 ticks
TX_NO_TIME_SLICE);
tx_thread_create(&task2, "Task2", task2_entry, 0,
stack2, STACK_SIZE,
10, 10, // 同优先级 10,时间片 5 ticks
TX_NO_TIME_SLICE);
注意:time_slice 参数的单位是 ThreadX 的 tick。默认 tick 是 10ms,所以 5 个 tick 就是 50ms。我建议时间片不要设太短,否则上下文切换开销会吃掉 CPU。一般 5-20 个 tick 比较合理。
优先级反转问题
优先级反转,是实时系统里最隐蔽的坑之一。简单说就是:高优先级任务被低优先级任务间接阻塞,而中优先级任务趁机抢跑。
典型的场景是这样的:
- 任务 L(低优先级)拿到信号量,开始访问共享资源
- 任务 H(高优先级)就绪,抢占 L,但发现信号量被 L 持有,于是挂起等待
- 任务 M(中优先级)就绪,它不需要信号量,于是抢跑执行
- 任务 L 因为被 M 抢占,一直无法释放信号量
- 任务 H 只能干等,直到 M 跑完,L 才能释放信号量
结果就是:最高优先级的任务 H,实际执行时间被中优先级任务 M 拖长了。这在实时系统里可能是致命的——比如电机控制任务错过控制周期,导致抖动甚至失控。
优先级继承协议
优先级继承,是 ThreadX 内置的解决方案。它的原理很简单:当高优先级任务等待低优先级任务持有的资源时,低优先级任务临时继承高优先级任务的优先级。这样中优先级任务就无法抢占低优先级任务了,高优先级任务就能尽快拿到资源。
ThreadX 的互斥量(Mutex)默认支持优先级继承。使用方式:
TX_MUTEX mutex;
// 创建互斥量,启用优先级继承
tx_mutex_create(&mutex, "ProtectMutex", TX_INHERIT);
// 任务 L 获取互斥量
tx_mutex_get(&mutex, TX_WAIT_FOREVER);
// ... 访问共享资源 ...
tx_mutex_put(&mutex);
// 任务 H 获取互斥量(如果 L 持有,L 会临时继承 H 的优先级)
tx_mutex_get(&mutex, TX_WAIT_FOREVER);
创建互斥量时,第二个参数传 TX_INHERIT 就开启了优先级继承。如果传 TX_NO_INHERIT,就是普通互斥量,不处理优先级反转。
| 特性 | 优先级继承 | 优先级天花板 |
|---|---|---|
| 原理 | 动态提升持有者的优先级 | 静态设定资源的最高优先级 |
| 实现复杂度 | 中等(需要跟踪等待关系) | 低(只需设定一个值) |
| 死锁风险 | 可能发生(需配合死锁检测) | 较低 |
| ThreadX 支持 | ✅ 默认支持 | ❌ 不直接支持 |
我个人更推荐优先级继承,因为它更灵活。但要注意:优先级继承不能完全避免死锁。如果两个任务互相等待对方持有的互斥量,还是会死锁。ThreadX 提供了 tx_mutex_get() 的超时参数,可以用来检测死锁——超时返回 TX_NOT_AVAILABLE。
调度策略对比总结
| 调度策略 | 适用场景 | 注意事项 |
|---|---|---|
| 纯抢占式 | 优先级层次分明的系统 | 注意优先级反转 |
| 时间片轮转 | 同优先级任务需要公平执行 | 时间片不宜过短 |
| 优先级继承 | 共享资源访问频繁的系统 | 注意死锁检测 |
核心知识结构图
下面这张图展示了 ThreadX 调度策略的核心关系:
嗯,调度策略这块内容不少,但核心就三条:抢占保证实时、时间片保证公平、优先级继承解决反转。实际项目中,我建议先画清楚任务的优先级关系,再决定哪些任务用时间片,哪些资源用互斥量。别一上来就全开,容易出问题。