4. 线程管理:线程创建、删除、挂起、恢复,线程优先级与栈空间配置
线程管理,说白了就是 RTOS 的心脏。你写的所有业务逻辑,最终都要靠一个个线程跑起来。ThreadX 在这方面做得相当轻量,非常适合资源受限的 MCU。我刚开始用 ThreadX 时,最直观的感受就是——API 少,但够用,而且每个函数的行为都很确定。
这一节,我们就把线程的整个生命周期走一遍。从创建到删除,中间穿插挂起和恢复,再聊聊优先级和栈空间怎么配才不翻车。
4.1 线程的创建与删除
创建线程,核心就一个函数:tx_thread_create。别看参数多,其实每个都有明确用途。我习惯先把参数列个表,写代码时对着填,不容易漏。
| 参数 | 含义 | 注意事项 |
|---|---|---|
thread_ptr |
线程控制块指针 | 必须全局或静态分配,不能是局部变量 |
name |
线程名称字符串 | 调试时很有用,建议起有意义的名称 |
entry_function |
线程入口函数 | 必须是 void (*)(ULONG) 类型 |
entry_input |
入口参数 | 通常传 0,或传一个结构体指针 |
stack_start |
栈起始地址 | 建议用 ALIGN 宏对齐到 4 或 8 字节 |
stack_size |
栈大小(字节) | 至少 512,复杂任务建议 1024 以上 |
priority |
优先级(0-31) | 0 最高,31 最低,默认 16 |
preempt_threshold |
抢占阈值 | 通常和优先级相同,高级用法 |
time_slice |
时间片(时钟滴答数) | 同优先级轮询调度时使用 |
auto_start |
是否自动启动 | TX_AUTO_START 或 TX_DONT_START |
下面是一个典型的创建示例。注意,我习惯把线程栈和控制块都放在全局数组里,这样地址固定,调试时也容易查看内存。
/* 定义线程控制块和栈 */
TX_THREAD my_thread;
ULONG my_thread_stack[1024 / sizeof(ULONG)]; /* 1024 字节栈 */
/* 线程入口函数 */
void my_thread_entry(ULONG initial_input)
{
/* 线程主循环 */
while(1)
{
/* 你的业务逻辑 */
tx_thread_sleep(10);
}
}
/* 创建线程 */
UINT status = tx_thread_create(
&my_thread, /* 控制块 */
"My Thread", /* 名称 */
my_thread_entry, /* 入口 */
0, /* 入口参数 */
my_thread_stack, /* 栈起始 */
sizeof(my_thread_stack), /* 栈大小 */
16, /* 优先级 */
16, /* 抢占阈值 */
TX_NO_TIME_SLICE, /* 时间片 */
TX_AUTO_START /* 自动启动 */
);
if (status != TX_SUCCESS)
{
/* 创建失败,处理错误 */
}
sizeof 计算,而不是硬编码。这样改数组大小时,创建函数自动同步,不会忘。
删除线程用 tx_thread_delete。但要注意,不能删除当前正在运行的线程。我见过有人直接在入口函数里调 tx_thread_delete 删自己,结果系统直接挂掉。正确的做法是:让线程退出循环,然后在外部任务或主函数里删除它。
/* 删除线程(必须在其他线程或主函数中调用) */
UINT status = tx_thread_delete(&my_thread);
if (status != TX_SUCCESS)
{
/* 删除失败,可能线程还在运行 */
}
4.2 线程的挂起与恢复
挂起和恢复,是线程控制的基本操作。挂起让线程暂停执行,恢复让它继续跑。ThreadX 提供了两个 API:tx_thread_suspend 和 tx_thread_resume。
挂起线程时,它会被移出就绪队列,调度器不会再选它执行。恢复时,它重新进入就绪队列,等待调度。
/* 挂起线程 */
UINT status = tx_thread_suspend(&my_thread);
/* 恢复线程 */
UINT status = tx_thread_resume(&my_thread);
这里有个细节:tx_thread_suspend 不能挂起当前正在运行的线程。如果你需要让当前线程暂停,应该用 tx_thread_sleep 或等待某个信号量。我早期犯过这个错,在中断里挂起当前线程,结果调度器直接死锁。
- 挂起:由外部控制,恢复时间不确定。
- 睡眠:线程自己调用,指定时间后自动唤醒。
4.3 线程优先级配置
ThreadX 支持 32 个优先级(0-31)。0 最高,31 最低。优先级决定了线程何时获得 CPU。高优先级线程就绪时,会立即抢占低优先级线程。
配置优先级时,我有一条铁律:不要把所有线程都设成同一个优先级。除非你明确需要时间片轮询,否则不同优先级的任务应该分开。比如,中断处理线程用高优先级(0-5),通信协议栈用中优先级(10-15),后台任务用低优先级(20-31)。
| 优先级范围 | 典型用途 | 注意事项 |
|---|---|---|
| 0-5 | 实时性要求极高的任务 | 尽量少用,避免阻塞其他任务 |
| 6-15 | 通信协议、传感器采集 | 中等实时性,可被高优先级抢占 |
| 16-25 | 数据处理、日志输出 | 允许一定延迟 |
| 26-31 | 后台维护、空闲任务 | 最低优先级,CPU 空闲时执行 |
优先级反转是个经典问题。低优先级线程持有资源,高优先级线程等待,结果中优先级线程抢跑,导致高优先级被阻塞。ThreadX 提供了优先级继承机制,但需要你手动开启。我建议在涉及互斥量的场景,务必启用优先级继承。
4.4 栈空间配置
栈空间是线程的“私人内存”。局部变量、函数调用、中断嵌套都用它。栈配小了,程序跑飞;配大了,内存浪费。在资源受限的 MCU 上,这个平衡尤其重要。
我一般按以下步骤确定栈大小:
- 估算:根据线程的局部变量和函数调用深度,估算一个初始值。简单任务 512 字节,复杂任务 1024 字节。
- 测试:跑起来后,用
tx_thread_stack_error_notify注册栈错误回调。ThreadX 会在栈溢出时调用这个函数。 - 调优:观察实际使用量,留出 20%-30% 的余量。然后缩小栈大小,节省内存。
/* 注册栈错误回调 */
void my_stack_error_handler(TX_THREAD *thread_ptr)
{
/* 记录错误,或重启线程 */
printf("Stack overflow in thread: %s\n", thread_ptr->tx_thread_name);
}
/* 在初始化时注册 */
tx_thread_stack_error_notify(my_stack_error_handler);
栈对齐也很关键。ARM Cortex-M 要求栈指针 8 字节对齐。ThreadX 内部会处理,但你的栈起始地址最好也对齐。用 ALIGN(8) 或 __attribute__((aligned(8))) 声明数组。
/* 8 字节对齐的栈 */
ULONG my_thread_stack[1024 / sizeof(ULONG)] __attribute__((aligned(8)));
4.5 知识结构图
下面这张图,把线程管理的核心概念串起来了。你可以看到线程从创建到删除的完整路径,以及挂起/恢复、优先级、栈空间之间的关系。
4.6 实战避坑指南
最后,我把这些年积累的线程管理经验,浓缩成几条避坑指南。每一条都是真金白银换来的。
- 不要在中断里创建或删除线程:ThreadX 的 API 大部分不是中断安全的。中断里只适合发信号量或事件标志。
- 栈大小不要用 sizeof(pointer):有人写
sizeof(ULONG *)当栈大小,结果栈只有 4 字节。用sizeof(array)最安全。 - 优先级不要设成 0:除非你确定这个线程必须立即响应。优先级 0 会阻塞所有其他任务,包括系统任务。
- 挂起线程前,确认它不在临界区:否则恢复后可能死锁。我遇到过挂起一个正在操作互斥量的线程,恢复后互斥量状态混乱。
- 删除线程后,检查所有资源是否释放:线程持有的信号量、队列、内存块,都需要手动清理。