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)
{
    /* 删除失败,可能线程还在运行 */
}
我曾经踩过的坑:删除线程后,对应的栈和控制块内存不会自动清零。如果你要复用这块内存,记得手动 memset。否则下次创建线程时,残留数据可能引发诡异问题。

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 或等待某个信号量。我早期犯过这个错,在中断里挂起当前线程,结果调度器直接死锁。

挂起 vs 睡眠:
  • 挂起:由外部控制,恢复时间不确定。
  • 睡眠:线程自己调用,指定时间后自动唤醒。
项目中,挂起常用于低功耗管理或任务同步。睡眠则用于周期性任务。

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 提供了优先级继承机制,但需要你手动开启。我建议在涉及互斥量的场景,务必启用优先级继承。

我的建议:在项目初期,先给每个线程分配一个“临时优先级”。跑起来后用 ThreadX 的调试工具观察线程切换情况,再调整。不要一开始就追求完美,调优是迭代出来的。

4.4 栈空间配置

栈空间是线程的“私人内存”。局部变量、函数调用、中断嵌套都用它。栈配小了,程序跑飞;配大了,内存浪费。在资源受限的 MCU 上,这个平衡尤其重要。

我一般按以下步骤确定栈大小:

  1. 估算:根据线程的局部变量和函数调用深度,估算一个初始值。简单任务 512 字节,复杂任务 1024 字节。
  2. 测试:跑起来后,用 tx_thread_stack_error_notify 注册栈错误回调。ThreadX 会在栈溢出时调用这个函数。
  3. 调优:观察实际使用量,留出 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);
我曾经踩过的坑:在 STM32F103 上,一个线程的栈配了 256 字节。局部变量里有个 128 字节的数组,再加两层函数调用,直接溢出。现象是程序随机跑飞,查了两天才定位到。从那以后,我每个线程的栈至少 512 字节起步。

栈对齐也很关键。ARM Cortex-M 要求栈指针 8 字节对齐。ThreadX 内部会处理,但你的栈起始地址最好也对齐。用 ALIGN(8) 或 __attribute__((aligned(8))) 声明数组。

/* 8 字节对齐的栈 */
ULONG my_thread_stack[1024 / sizeof(ULONG)] __attribute__((aligned(8)));

4.5 知识结构图

下面这张图,把线程管理的核心概念串起来了。你可以看到线程从创建到删除的完整路径,以及挂起/恢复、优先级、栈空间之间的关系。

线程管理知识结构图 线程 (Thread) 创建 tx_thread_create 删除 tx_thread_delete 挂起 tx_thread_suspend 恢复 tx_thread_resume 优先级 (0-31) 栈空间配置 核心原则:栈大小留余量,优先级分层次, 挂起/恢复用于同步,创建/删除注意生命周期。

4.6 实战避坑指南

最后,我把这些年积累的线程管理经验,浓缩成几条避坑指南。每一条都是真金白银换来的。

  • 不要在中断里创建或删除线程:ThreadX 的 API 大部分不是中断安全的。中断里只适合发信号量或事件标志。
  • 栈大小不要用 sizeof(pointer):有人写 sizeof(ULONG *) 当栈大小,结果栈只有 4 字节。用 sizeof(array) 最安全。
  • 优先级不要设成 0:除非你确定这个线程必须立即响应。优先级 0 会阻塞所有其他任务,包括系统任务。
  • 挂起线程前,确认它不在临界区:否则恢复后可能死锁。我遇到过挂起一个正在操作互斥量的线程,恢复后互斥量状态混乱。
  • 删除线程后,检查所有资源是否释放:线程持有的信号量、队列、内存块,都需要手动清理。
一句话总结:线程管理就是“创建-运行-挂起/恢复-删除”的生命周期管理。优先级和栈空间是配置的核心。记住,每个线程都是一条独立的执行流,别让它们互相踩脚。