线程相关函数(1)

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 1 Likes: 0 Comments: 0

253.2.4线程相关函数(1)

#include <rtthread.h>                         // 引入 RT-Thread 的线程类型、错误码和线程管理接口。
#define DBG_TAG "main"                       // 把本文件的日志标签设置为 main。
#define DBG_LVL DBG_LOG                      // 允许输出调试级别及以上的日志。
#include <rtdbg.h>                            // 在日志配置宏之后引入 RT-Thread 日志接口。
rt_thread_t th1_ptr, th2_ptr;                // 分别保存动态线程 th1 和 th2 的句柄。
void th1_printf(void *parameter)              // 定义 th1 的入口函数;本节没有使用入口参数。
{                                             // th1 入口函数开始。
    rt_thread_t current_ptr;                  // 用来保存当前正在运行线程的句柄。
    while (1)                                 // 让 th1 周期性地持续运行。
    {                                         // th1 循环体开始。
        current_ptr = rt_thread_self();        // 取得调用此函数的当前线程句柄,此处应得到 th1 的句柄。
        rt_kprintf("the thread of name is: %s \r\n", current_ptr->parent.name); // 通过线程控制块中的内核对象名称打印当前线程名。
        rt_kprintf("In %s,func %s is running! the line is %d!\r\n", __FILE__, __FUNCTION__, __LINE__); // 输出当前源文件、函数名和代码行号。
        rt_thread_mdelay(1000);                // 让 th1 挂起约 1000 ms,把 CPU 留给其他就绪线程。
    }                                         // th1 本轮循环结束。
}                                             // th1 入口函数结束。
void th2_printf(void *parameter)              // 定义 th2 的入口函数;本节同样没有使用入口参数。
{                                             // th2 入口函数开始。
    while (1)                                 // 让 th2 周期性地持续运行。
    {                                         // th2 循环体开始。
        rt_kprintf("In %s,func %s is running! the line is %d!\r\n", __FILE__, __FUNCTION__, __LINE__); // 输出 th2 当前所在的文件、函数和代码行。
        rt_thread_mdelay(1000);                // 让 th2 挂起约 1000 ms,使其他就绪线程能够运行。
    }                                         // th2 本轮循环结束。
}                                             // th2 入口函数结束。
int main(void)                                // RT-Thread 初始化完成后,主线程从 main 开始执行。
{                                             // main 函数开始。
    int count = 1;                            // 作为课程模板中主循环的计数变量。
    th1_ptr = rt_thread_create("th1",         // 动态创建名称为 th1 的线程,并保存返回的线程句柄。
                               th1_printf,    // 指定 th1_printf 为 th1 的入口函数。
                               NULL,          // 本节不向 th1 传递入口参数。
                               1024,          // 由内核从堆中为 th1 分配 1024 字节线程栈。
                               20,            // 把 th1 的优先级设置为 20;数值越小,优先级越高。
                               10);           // 把 th1 的初始时间片设置为 10 个系统 tick。
    if (th1_ptr == RT_NULL)                   // 返回空句柄表示动态创建 th1 失败。
    {                                         // th1 创建失败分支开始。
        LOG_E("th1_printf rt_thread_create failed!\r\n"); // 输出 th1 创建失败日志。
        return -RT_ENOMEM;                    // 按课程源码返回负的“内存不足”错误码。
    }                                         // th1 创建失败分支结束。
    rt_thread_startup(th1_ptr);               // 启动 th1,使其进入就绪队列并参与调度。
    th2_ptr = rt_thread_create("th2",         // 动态创建名称为 th2 的线程,并保存返回的线程句柄。
                               th2_printf,    // 指定 th2_printf 为 th2 的入口函数。
                               NULL,          // 本节不向 th2 传递入口参数。
                               1024,          // 由内核从堆中为 th2 分配 1024 字节线程栈。
                               20,            // 把 th2 的优先级也设置为 20,以观察同优先级线程调度。
                               10);           // 把 th2 的初始时间片设置为 10 个系统 tick。
    if (th2_ptr == RT_NULL)                   // 返回空句柄表示动态创建 th2 失败。
    {                                         // th2 创建失败分支开始。
        LOG_E("th2_printf rt_thread_create failed!\r\n"); // 输出 th2 创建失败日志。
        return -RT_ENOMEM;                    // 按课程源码返回负的“内存不足”错误码。
    }                                         // th2 创建失败分支结束。
    rt_thread_startup(th2_ptr);               // 启动 th2,使其进入就绪队列并参与调度。
    while (count++)                           // 按课程源码持续运行主线程循环。
    {                                         // 主线程循环开始。
        LOG_D("Hello RT-Thread!");            // 输出主线程仍在运行的调试日志。
        rt_thread_mdelay(1000);                // 让主线程挂起约 1000 ms,避免持续占用 CPU。
    }                                         // 主线程循环结束。
    return RT_EOK;                            // 理论上的成功返回;正常运行时不会执行到这里。
}                                             // main 函数结束。

一、线程控制函数

本节讲解线程休眠、挂起、恢复、获取当前线程以及主动触发调度等接口。课程使用的工程为 RT-Thread 5.0.2;下面的内部流程均按本项目配套源码核对,不能机械套用到所有版本。

视频开头先在上一节工程的 MSH 终端执行 list thread,观察线程状态;随后新建 05CONCTRL 工程,并将两个业务线程都改为动态创建。开头状态表中的 th2 优先级为 21,属于上一节工程;本节最终代码重新创建的 th1、th2 优先级都为 20,不应把两处配置混为一谈。

1.1 线程休眠函数

线程休眠是指:当前线程在指定时间内主动变为挂起状态,不再参加就绪调度;时间到达后,内核先把它恢复为就绪状态,至于何时真正运行,还要由调度器根据优先级和同优先级队列决定。

本工程可以在 MSH 中执行:

list thread

课程画面中的主要列含义如下:

列含义课程画面的读法
rt_thread_t线程控制块地址,即线程句柄的数值每个线程地址不同
thread线程名称th1、th2、tshell、tidle0、timer、main
pri当前优先级本项目范围为 0~31,数字越小优先级越高
status当前状态执行延时的线程显示 suspend;正在执行命令的 tshell 显示 running;空闲线程显示 ready
sp当前栈使用位置的统计值;在本项目向下生长的栈上,命令输出的是当前已用栈字节数例如 0x000000bc 是十六进制的 188 字节,并不是线程栈总大小
stack size分配给线程的栈总大小0x00000400 = 1024 字节
max used从栈填充值扫描得到的历史最大使用率,即栈高水位可用来辅助判断栈是否接近溢出,不能只看某一时刻的局部变量大小
left tick当前线程剩余时间片0x0000000a = 10 tick;它不是休眠还剩多少 tick
error线程当前保存的错误状态延时中的线程可暂时显示 EINTRPT,对应睡眠流程写入的 -RT_EINTR,不等于本次实验已经失败

画面中 tshell 正在运行,是因为它正在执行并打印 list thread;空闲线程 tidle0 的优先级为最低的 31,并保持就绪,保证没有其他线程可运行时 CPU 仍有合法的线程上下文。优先级为 4 的 timer 是软件定时器线程,当没有软件定时器回调需要处理时可以处于挂起状态。

特别注意:休眠时长与线程时间片是两套不同的计时概念。 创建线程时的 10 是同优先级轮转所用的时间片;rt_thread_mdelay(1000) 表示线程挂起约 1000 ms。休眠线程是因其内置定时器到期而被唤醒,不是等待 remaining_tick 耗尽。

1.1.1 rt_thread_mdelay

接口原型为:

rt_err_t rt_thread_mdelay(rt_int32_t ms);

ms 的单位是毫秒。它与裸机中常见的忙等待 delay 有本质区别:忙等待会让 CPU 在循环中空转,而 rt_thread_mdelay() 会把当前线程挂起,调度器可以运行其他就绪线程。

在本项目 RT-Thread 5.0.2 中,其源码逻辑可概括为:

  1. 调用 rt_tick_from_millisecond(ms),把毫秒数换算为 OS tick;
  2. 将换算得到的 tick 传给 rt_thread_sleep();
  3. 返回 rt_thread_sleep() 的执行结果。

本项目 RT_TICK_PER_SECOND 为 1000,所以名义上:

$ 1\ \text{tick}=\frac{1}{1000}\ \text{s}=1\ \text{ms} $

因此 rt_thread_mdelay(1000) 在本工程中会换算为 1000 tick。这个换算关系由项目配置决定,并不是 RT-Thread 在所有工程中的固定值。

rt_thread_sleep() 的内部流程如下:

取得当前线程
    ↓
关中断,清除旧错误状态
    ↓
将当前线程设为可中断挂起态,并移出调度队列
    ↓
设置并启动线程控制块内置的一次性定时器
    ↓
恢复中断,调用 rt_schedule() 运行其他就绪线程
    ↓
定时器到期:_thread_timeout() 把该线程移回就绪队列
    ↓
线程再次被调度后,从 rt_thread_sleep() 后续位置继续执行

原笔记把该定时器写成固定插入 soft_timer_list,也把唤醒写成直接调用 rt_thread_resume(),这两点与配套 5.0.2 源码不符:

  • 每个线程的 thread_timer 初始化标志是 RT_TIMER_FLAG_ONE_SHOT,没有 RT_TIMER_FLAG_SOFT_TIMER,所以 rt_timer_start() 将它插入系统定时器表 _timer_list;即使工程启用了软件定时器线程,也不代表所有定时器都是软定时器。
  • 到期回调 _thread_timeout() 会设置超时状态、从挂起链表移除线程、调用 rt_schedule_insert_thread() 加入就绪队列,然后触发调度;它没有调用公开接口 rt_thread_resume()。

延时结束只保证线程重新具备运行条件,并不保证它在精确的那个 CPU 指令时刻继续执行。系统 tick 的量化、关中断时段以及更高优先级线程都可能使实际恢复运行时间稍晚,因此它适合 RTOS 任务级延时,不应当作微秒级精密忙等待。

1.1.2 rt_thread_sleep

接口原型为:

rt_err_t rt_thread_sleep(rt_tick_t tick);

tick 的单位是 OS tick,不是毫秒,也不是线程的“时间片个数”。在当前 1000 Hz 配置下数值恰好可与毫秒一一对应,但代码若移植到其他 tick 频率,二者就未必相同。

本版本实现中,tick == 0 会直接返回 -RT_EINVAL。对于有效参数,函数只作用于调用它的当前线程:它取得 rt_thread_self(),挂起该线程,重设并启动线程内置定时器,再调用调度器。定时器到期后,线程由挂起态进入就绪态,稍后从 rt_schedule() 返回点继续运行。

休眠过程中,线程保存的 error 会暂时被设为 -RT_EINTR,所以恰好在这时执行 list thread 可能看到 EINTRPT。正常定时到期时,超时回调先写入 -RT_ETIMEOUT,睡眠函数恢复执行后再把它清成 RT_EOK。因此状态表中的临时错误文本要结合线程所处阶段理解。

1.1.3 rt_thread_delay

接口原型为:

rt_err_t rt_thread_delay(rt_tick_t tick);

在本项目的 RT-Thread 5.0.2 源码中,函数体就是返回 rt_thread_sleep(tick),所以 rt_thread_delay() 与 rt_thread_sleep() 的参数单位和实际行为相同。rt_thread_mdelay() 则多了一步“毫秒转换为 tick”。三者关系可以归纳为:

接口参数单位本项目 5.0.2 的关系
rt_thread_sleep(tick)OS tick完整的挂起、定时和调度实现
rt_thread_delay(tick)OS tick直接调用 rt_thread_sleep(tick)
rt_thread_mdelay(ms)ms先将 ms 换算为 tick,再调用 rt_thread_sleep()

课程使用 Ctrl+H 在工作区搜索函数定义。这种方法适合沿着包装函数继续追到真正实现;源码行号会随版本变化,所以“位于第 674 行”不应作为知识点,函数名、版本和调用关系才是可靠定位依据。原笔记中的“RTC 的 delay”属于语音识别错误,这里讨论的是 RT-Thread 的三个线程休眠接口。

1.2 挂起和唤醒线程

本项目 5.0.2 中公开挂起接口为:

rt_err_t rt_thread_suspend(rt_thread_t thread);

它只是以 RT_UNINTERRUPTIBLE 标志调用 rt_thread_suspend_with_flag()。配套源码还用断言要求 thread == rt_thread_self(),也就是说在该工程中只能让当前线程挂起,不能拿另一个线程句柄直接暂停对方。

挂起实现会在关中断保护下检查状态、把当前线程移出调度关系、设置挂起状态并停止其内置定时器。但是 rt_thread_suspend() 本身不调用 rt_schedule()。如果当前线程确实主动挂起自己,随后必须立即触发调度,否则已经标记为挂起的线程仍会沿当前代码继续执行,造成状态与执行流不一致。

直接挂起还有一个必须事先解决的问题:谁来恢复它。若线程把自己挂起后,再没有其他线程、中断处理或超时机制持有其句柄并执行恢复,它就不会自行回来。实际业务通常优先通过信号量、事件、邮箱、消息队列或带超时的等待接口表达“等待条件”,而不是手工拼接挂起与恢复。

1.2.1 唤醒

恢复接口为:

rt_err_t rt_thread_resume(rt_thread_t thread);

它要求目标线程当前确实处于挂起状态。执行成功后主要完成:

  1. 从目标线程所在的挂起链表中移除其节点;
  2. 停止目标线程可能仍在运行的内置定时器;
  3. 把目标线程插入相应优先级的就绪队列。

rt_thread_resume() 只把线程恢复为就绪态,并不直接把它改成运行态;也不会在该函数内部调用 rt_schedule()。是否立即发生抢占,取决于调用位置、之后是否请求调度以及被恢复线程的优先级。线程上下文中若要求立刻重新比较优先级,调用方还需在合适位置触发调度;若在中断上下文中恢复线程,则应由相应的内核或驱动调用路径按中断调度规则请求切换,不能仅凭一次 rt_thread_resume() 就假定目标线程会立即运行。

“唤醒”在口语中容易让人误以为目标线程立即执行。准确过程应记为:

挂起线程 ── rt_thread_resume() / 延时到期 / 等待条件满足 ──► 就绪线程
就绪线程 ── 被调度器选中 ──► 运行线程

1.3 获取当前线程

接口原型为:

rt_thread_t rt_thread_self(void);

它没有参数,返回当前 CPU 正在执行的线程句柄。rt_thread_t 本质上是 struct rt_thread *,所以课程代码把返回值保存到 current_ptr 后,可以沿着线程控制块中内嵌的内核对象成员读取名称:

current_ptr
    └── parent          struct rt_object
          └── name     内核对象名称

因此 current_ptr->parent.name 得到当前线程名。th1_printf() 是在线程调度已经启动后由 th1 执行的,所以这里返回有效的 th1 句柄,串口打印 the thread of name is: th1。

官方接口也规定:如果调度器尚未启动,rt_thread_self() 可能返回 RT_NULL。所以“永远不用判断返回值”并不严谨。本例位于已运行的线程入口中,可以依赖上下文得到有效句柄;若编写可能在内核早期初始化阶段调用的通用函数,则应先确认返回值再解引用。

直接访问 parent.name 展示了 TCB 的内部层级,适合本节学习和调试;业务代码不应因此随意改写 parent、状态、优先级或链表节点等内核内部字段。

1.4 应用案例

本节在新工程中动态创建 th1、th2,两者栈大小均为 1024 字节、优先级均为 20、初始时间片均为 10 tick。它们与主线程每轮打印后都会休眠 1000 ms,所以串口能够交替看到三个线程的输出。

1.4.1 例题: 线程名字打印

课程只在 th1_printf() 中增加以下业务逻辑:先定义 rt_thread_t current_ptr,循环中调用 rt_thread_self(),再通过 current_ptr->parent.name 打印名称。th2_printf() 没有加入这段打印,所以输出中的当前线程名只显示 th1。

这段实验还能说明:

  • “当前线程”是调用瞬间正在 CPU 上执行的线程,而不是某个固定全局线程;同一辅助函数若被不同线程调用,rt_thread_self() 会分别返回各自句柄。
  • rt_thread_self() 返回句柄,不返回新分配的对象;应用不需要也不能释放这个返回值。
  • 线程名称用于调试和对象识别,不决定优先级或调度顺序。
  • 两个优先级相同且延时时间相同的线程,其打印先后仍受启动时机、tick 到期顺序和调度状态影响,不应把一次串口顺序当作永久保证。
  • 顶部代码中的两个入口形参 parameter 没有被使用,这是课程源码的当前状态;编译器若启用未使用参数警告,可能提示该形参未被引用。

代码中的 while (count++) 也是课程现场原样保留。count 为有符号 int,持续自增最终会溢出,而 C 语言中的有符号溢出是未定义行为;真正需要永久循环时通常明确写 while (1)。这里仅指出风险,不擅自改写视频源码。

二、线程控制函数

2.1 线程切换

课程比较了两个容易混淆的接口:

void rt_schedule(void);
rt_err_t rt_thread_yield(void);

rt_schedule() 的含义是“立即执行一次调度决策”,而不是“保证把当前线程切走”。调度器会比较当前线程和就绪队列中的候选线程:

  • 有更高优先级线程已经就绪时,选择更高优先级线程;
  • 没有更高优先级线程,且当前线程仍可运行时,普通的 rt_schedule() 可能仍选择当前线程;
  • 当前线程已经因延时、IPC 等原因挂起时,才必须从其他就绪线程中选择;
  • 调度器被锁定时,本次调用也不会按普通路径完成线程切换。

rt_thread_yield() 则明确表示当前线程愿意让同优先级的其他就绪线程先运行。若不存在另一个同优先级就绪线程,调用者仍会继续执行,并不会因此让低优先级线程获得 CPU。

因此,二者的准确差异不是简单的“一个让出、一个不排队”,而是:rt_schedule() 只要求重新做一次最高优先级选择;rt_thread_yield() 额外标记了当前线程的主动让出意图,使同优先级候选线程可以在本次调度中取代它。

2.2 让出CPU

在基于优先级的抢占式调度中,“让出 CPU”必须说明让给谁:

场景结果
更高优先级线程就绪更高优先级线程本来就应在调度点抢占当前线程,不依赖 yield 才获得 CPU
存在同优先级就绪线程rt_thread_yield() 可让下一个同优先级线程先运行,当前线程仍保持可运行状态
只有低优先级线程就绪当前高优先级线程只调用 yield 仍会继续运行;低优先级线程不能越过仍然就绪的高优先级线程

所以,RT-Thread 没有一个能让仍处于就绪态的高优先级线程“指定低优先级线程运行一次”的普通接口。若高优先级线程希望低优先级线程获得执行机会,它必须暂时变为不可运行,例如:

  • 调用延时函数进入挂起态;
  • 等待信号量、事件、邮箱、消息队列或设备数据;
  • 主动挂起并由其他执行流恢复;
  • 结束线程,或者在确有设计依据时调整优先级。

线程入口可以使用 while (1)。真正的问题是高优先级循环若始终不阻塞,会一直保持最高优先级可运行状态,使低优先级线程饥饿,同时造成 CPU 空转和功耗上升。yield 只能改善同优先级线程之间的轮转,不能替代合理的阻塞设计。

2.3 应用案例

2.3.1 例题: 线程切换函数使用

这一小节在视频中是源码查找演示,并没有向 main.c 添加 rt_schedule() 或 rt_thread_yield() 调用。课程用 Ctrl+H 搜索接口并进入内核实现,以便理解其行为。顶部代码因此只收录视频实际完成的 rt_thread_self() 线程名实验,不凭空补写一个“线程切换示例”。

阅读此类内核接口时,应按以下顺序追踪:先看公开声明与注释,再看包装函数,继续追到调度器和就绪队列操作,最后结合当前工程配置判断走到哪个条件编译分支。只看函数名或某一行注释,很容易把“触发调度”误解成“必然发生上下文切换”。

2.3.2 线程切换原理分析

本项目 RT-Thread 5.0.2 的 rt_thread_yield() 主要完成四步:

  1. 调用 rt_thread_self() 取得当前线程;
  2. 关闭中断,把 remaining_tick 恢复为 init_tick,并在 stat 中置位 RT_THREAD_STAT_YIELD;
  3. 恢复先前的中断状态;
  4. 调用 rt_schedule() 重新调度。

关中断保护的是“更新时间片和让出标志”这一小段共享调度状态,防止更新到一半被系统 tick 或其他中断打断。rt_schedule() 内部还会再次关中断,保护后续候选线程选择、就绪队列修改和上下文切换准备。这里的关中断是单核内核的短临界区手段,不等同于可长时间持有的互斥锁。

本版本单核调度器中,正在运行的线程并不作为普通节点留在就绪链表内。设置 YIELD 标志后,如果就绪队列中存在同优先级线程,rt_schedule() 会允许该线程成为新的运行线程,并把原线程重新纳入就绪调度;若没有同优先级候选者,当前线程仍是最高优先级可运行者,就不会发生有意义的上下文切换。

官方文档常把 yield 的抽象效果描述成“当前线程回到同优先级队列末尾,让下一个同优先级线程运行”。学习源码时不要把这句话误写成“rt_thread_yield() 函数体直接删除并重新插入链表”:在本项目 5.0.2 中,它的函数体只更新时间片、设置标志并调用调度器,真正的候选比较和队列处理发生在 scheduler_up.c 的 rt_schedule() 中。

还要分清主动让出与时间片耗尽:系统 tick 处理会递减当前线程的 remaining_tick;减到 0 后同样重装时间片并触发同优先级轮转。主动调用 rt_thread_yield() 相当于不等当前时间片自然耗尽就提出这次轮转请求,但它不会把线程变成挂起态。

核对依据:本项目配套 RT-Thread 5.0.2 的 rt-thread/src/thread.c、rt-thread/src/scheduler_up.c、rt-thread/src/timer.c、rt-thread/components/finsh/cmd.c、rt-thread/include/rtdef.h 与 rtconfig.h,以及 RT-Thread 官方线程管理文档、RT-Thread v5.0.2 thread.c、RT-Thread v5.0.2 scheduler_up.c和RT-Thread v5.0.2 cmd.c。