4. 线程管理与调度实战

在 RT-Thread 中,线程是最小的调度单位,也是量产项目里最常被直接操作的对象。理解线程的完整生命周期,比记住几个 API 更重要:一个线程从创建到删除,中间会经历就绪、运行、挂起、关闭等状态,而调度器则根据优先级和时间片在这些状态之间做决策。本章以“多线程 LED 与串口”案例为主线,把线程创建、删除、挂起、恢复、优先级和时间片轮转串起来,并落到线程通信的实战上。

一、核心原理:线程状态与调度器

RT-Thread 的线程有五个基本状态:初始态(RT_THREAD_INIT)、就绪态(RT_THREAD_READY)、运行态(RT_THREAD_RUNNING)、挂起态(RT_THREAD_SUSPEND)和关闭态(RT_THREAD_CLOSE)。创建线程后它进入初始态,调用 rt_thread_startup() 后进入就绪态;调度器从就绪队列中选出优先级最高的线程运行。当高优先级线程就绪时,会立即抢占当前低优先级线程,这就是 RT-Thread 的抢占式调度。

优先级数值越小,优先级越高,RT-Thread 默认支持 0~31 共 32 个优先级,其中 0 最高。相同优先级的线程之间采用时间片轮转:每个线程被分配一个时间片(tick),运行完自己的时间片后让出 CPU,排到同优先级就绪队列的末尾。因此“优先级决定谁先跑,时间片决定同优先级谁跑多久”。

关键结论:优先级用于区分紧急程度,时间片用于同优先级公平轮转;挂起/恢复用于临时让出或重新参与调度,删除用于彻底释放资源。

二、方法流程:线程操作的标准动作

线程操作有一套固定流程,建议按“创建 → 启动 → 运行 → 挂起/恢复 → 删除”的顺序理解。创建分为动态和静态两种:动态使用 rt_thread_create(),从堆中分配线程控制块和栈;静态使用 rt_thread_init(),由用户提供线程控制块和栈空间,适合对内存确定性要求高的量产项目。

/* 动态创建线程 */
rt_thread_t tid = rt_thread_create("led",
                                   led_thread_entry,
                                   RT_NULL,
                                   512,        /* 栈大小 */
                                   20,         /* 优先级 */
                                   10);        /* 时间片 */
if (tid != RT_NULL)
    rt_thread_startup(tid);

/* 挂起与恢复 */
rt_thread_suspend(tid);
rt_thread_resume(tid);

/* 删除线程 */
rt_thread_delete(tid);

挂起和恢复常用于“临时停止某个任务”的场景,例如串口升级时暂停 LED 闪烁。需要注意:挂起一个正在运行的线程后,必须由其他线程调用 rt_thread_resume() 才能恢复;如果线程自己挂起自己,则必须等待外部事件唤醒。删除线程时,动态创建的线程由系统回收栈和线程控制块,静态线程则需要先 rt_thread_detach() 再自行释放资源。

三、案例场景:多线程 LED 与串口通信

下面用一个典型量产场景说明:系统中有三个线程——LED 闪烁线程(优先级 20)、串口接收线程(优先级 15)、串口命令解析线程(优先级 18)。串口接收线程通过信号量通知解析线程,解析线程根据命令控制 LED 线程的挂起与恢复。这样既演示了优先级抢占,也演示了线程间通信。

/* 串口接收线程:收到数据释放信号量 */
void uart_rx_thread_entry(void *param)
{
    while (1) {
        rt_size_t len = rt_device_read(uart_dev, 0, buf, sizeof(buf));
        if (len > 0) {
            rt_mb_send(&cmd_mb, (rt_ubase_t)buf);
        }
        rt_thread_mdelay(10);
    }
}

/* 命令解析线程:根据命令挂起/恢复 LED 线程 */
void cmd_thread_entry(void *param)
{
    rt_ubase_t msg;
    while (1) {
        if (rt_mb_recv(&cmd_mb, &msg, RT_WAITING_FOREVER) == RT_EOK) {
            if (msg == CMD_LED_OFF)
                rt_thread_suspend(led_tid);
            else if (msg == CMD_LED_ON)
                rt_thread_resume(led_tid);
        }
    }
}

运行时可观察到:串口接收线程优先级最高,收到数据后立即抢占;解析线程优先级居中,处理命令;LED 线程优先级最低,在空闲时闪烁。当发送“LED 关闭”命令后,LED 线程被挂起,不再参与调度;发送“LED 开启”后恢复。整个过程体现了优先级、挂起恢复和线程通信的协同。

四、参数与行为对照

操作API状态变化典型用途
动态创建rt_thread_create初始态 → 就绪态常规任务,灵活分配
静态创建rt_thread_init初始态 → 就绪态内存确定性要求高
启动rt_thread_startup初始态 → 就绪态让线程参与调度
挂起rt_thread_suspend就绪/运行 → 挂起态临时停止任务
恢复rt_thread_resume挂起态 → 就绪态重新参与调度
删除rt_thread_delete任意 → 关闭态释放动态资源
脱离rt_thread_detach任意 → 关闭态静态线程释放

五、误区与复盘

常见误区一:把优先级当成“执行顺序”。优先级只决定抢占关系,同优先级才由时间片决定轮转顺序。误区二:在线程中调用 rt_thread_delete 删除自身,可能导致资源回收异常,应让其他线程删除或使用 rt_thread_exit。误区三:挂起线程后忘记恢复,导致任务永久不运行。误区四:时间片设置过小,频繁切换增加开销;设置过大,同优先级响应变慢。

复盘建议:在量产项目中,优先使用静态创建保证内存可控;对实时性要求高的串口接收线程给较高优先级;对 LED 这类非关键任务给较低优先级并配合时间片;挂起/恢复要成对出现,最好用状态机或命令表管理。掌握这些,线程管理就不再是零散 API,而是一套可复用的工程方法。

RT-Thread 线程状态迁移与调度关系 初始态 INIT 就绪态 READY 运行态 RUNNING 挂起态 SUSPEND 关闭态 CLOSE startup 调度选中 delete/detach suspend resume 同优先级时间片轮转(优先级 20) 线程A 10tick 线程B 10tick 线程A 10tick 线程B 10tick 时间 高优先级线程就绪时立即抢占;同优先级按时间片依次轮转。

把这张状态迁移图记在脑子里,再回头看 rt_thread_create、rt_thread_suspend、rt_thread_resume 这些 API,就能清楚每个调用把线程推到了哪个状态、由谁负责把它拉回来。多线程 LED 与串口案例的价值,正在于把抽象的状态机变成可观察、可调试的真实行为。