静态线程创建

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

251.2.2静态线程创建

#include <rtthread.h>                         // 引入 RT-Thread 的线程类型、状态码和线程管理 API。
#define DBG_TAG "main"                       // 把本文件的日志标签设置为 main。
#define DBG_LVL DBG_LOG                      // 允许输出调试级别及以上的日志。
#include <rtdbg.h>                            // 在日志配置宏之后引入 RT-Thread 日志接口。
rt_thread_t th1_ptr;                          // 保存动态线程 th1 的句柄;rt_thread_t 本质上是结构体指针类型。
struct rt_thread th2_ptr;                     // 为静态线程 th2 直接定义一个线程控制块对象;变量名虽含 ptr,但它不是指针。
rt_uint8_t th2_stack[1024];                   // 为静态线程 th2 预留 1024 字节的线程栈。
int ret;                                      // 按课程源码定义整型变量,接收 rt_thread_init() 返回的状态码。
void th1_printf(void *parameter)              // 定义动态线程 th1 的入口函数;本节没有使用 parameter。
{                                             // th1 入口函数开始。
    while (1)                                 // 让 th1 持续运行。
    {                                         // th1 循环体开始。
        rt_kprintf("In %s,func %s is running! the line is %d!\r\n", __FILE__, __FUNCTION__, __LINE__); // 输出源文件、当前函数和当前代码行。
        rt_thread_mdelay(1000);                // 让 th1 延时阻塞约 1000 ms,使其他就绪线程可以运行。
    }                                         // th1 本轮循环结束。
}                                             // th1 入口函数结束。
void th2_printf(void *parameter)              // 定义静态线程 th2 的入口函数;本节没有使用 parameter。
{                                             // th2 入口函数开始。
    // int count = 0;                         // 删除实验使用的计数变量;课程结束时已将它注释。
    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,主动让出 CPU。
        // count++;                            // 删除实验中,每次延时结束后把计数值加 1。
        // if (count == 5)                     // 删除实验中,只在计数等于 5 时执行一次删除。
        //     rt_thread_delete(th1_ptr);       // 删除由 rt_thread_create() 动态创建的 th1;课程结束时已注释。
    }                                         // th2 本轮循环结束。
}                                             // th2 入口函数结束。
int main(void)                                // RT-Thread 初始化完成后,主线程从 main 开始执行。
{                                             // main 函数开始。
    int count = 1;                            // 作为课程模板中主循环的计数变量。
    th1_ptr = rt_thread_create("th1",         // 动态创建名为 th1 的线程,并保存返回的线程句柄。
                               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,使它进入就绪队列并参与调度。
    ret = rt_thread_init(&th2_ptr,             // 初始化用户提供的 th2 线程控制块,并接收执行状态。
                         "th2",               // 把静态线程名称设置为 th2。
                         th2_printf,          // 指定 th2 的入口函数。
                         NULL,                // 本节不向 th2 传递入口参数。
                         th2_stack,           // 把用户预留的栈数组首地址交给内核。
                         1024,                // 指明 th2 可使用的栈空间为 1024 字节。
                         21,                  // 按课程最终实验把 th2 优先级设置为 21,低于 th1 的 20。
                         10);                 // 把 th2 的初始时间片设置为 10 个系统 tick。
    if (ret != RT_EOK)                        // 返回值不是 RT_EOK,表示静态线程初始化失败。
    {                                         // th2 初始化失败分支开始。
        LOG_E("th2_printf rt_thread_create failed!\r\n"); // 原样保留视频中的日志文本;实际调用的是 rt_thread_init()。
        return ret;                           // 将 rt_thread_init() 给出的错误码返回给上层。
    }                                         // th2 初始化失败分支结束。
    rt_thread_startup(&th2_ptr);              // 传入静态控制块的地址,启动 th2 并使其参与调度。
    while (count++)                           // 按课程源码持续运行主线程循环。
    {                                         // 主线程循环开始。
        // LOG_D("Hello RT-Thread!");          // 课程结束时关闭主线程日志,只保留延时以避免主线程空转。
        rt_thread_mdelay(1000);                // 让主线程延时阻塞约 1000 ms,给其他线程运行机会。
    }                                         // 主线程循环结束。
    return RT_EOK;                            // 理论上的成功返回;正常运行时不会执行到这里。
}                                             // main 函数结束。

一、静态线程创建

本节在上一课动态创建 th1 的基础上,新增静态线程 th2,并通过几个小实验观察线程删除、调试宏、优先级抢占和阻塞延时。上面的代码按视频结束前的最终状态整理;课程配套压缩包中的工程是连续更新的,当前 04RTTHREAD 工程已经包含下一节的线程参数传递代码,因此不能直接用压缩包中的最终文件替换本节代码。

1.1 静态线程创建过程

“静态线程”并不是指线程不能变化或不能被调度,而是指 线程控制块和线程栈由应用程序预先提供。本例为 th2 准备了两块具有静态存储期的内存:

  • struct rt_thread th2_ptr;:线程控制块(Thread Control Block,TCB),保存线程状态、优先级、栈指针等管理信息。尽管变量名是 th2_ptr,它本身仍是一个结构体对象,所以传参时必须写成 &th2_ptr。
  • rt_uint8_t th2_stack[1024];:线程运行时使用的独立栈空间。该数组是全局变量,在整个程序运行期间一直有效,不会像普通函数中的局部数组那样在函数返回后失效。

动态线程 th1 与静态线程 th2 的内存来源如下:

对比项动态创建 rt_thread_create()静态初始化 rt_thread_init()
线程控制块内核从动态内存中分配用户定义 struct rt_thread 对象
线程栈内核按 stack_size 从动态内存中分配用户定义栈数组并传入首地址和大小
成功结果返回 rt_thread_t 线程句柄返回 RT_EOK 状态码
结束接口rt_thread_delete()rt_thread_detach()
是否依赖系统堆依赖 RT_USING_HEAP不依赖运行时动态分配线程内存

rt_thread_t 在本项目的 RT-Thread 5.0.2 源码中定义为 struct rt_thread *。因此:

  • rt_thread_t th1_ptr 是一个指针变量,动态创建成功后指向内核分配的 TCB;
  • struct rt_thread th2_ptr 是一个真正的 TCB 对象,&th2_ptr 才是指向它的指针;
  • th2_stack 是另一块独立内存,不能与 TCB 混为一谈。

本例调用为:

ret = rt_thread_init(&th2_ptr, "th2", th2_printf, NULL, th2_stack, 1024, 21, 10);

八个参数的意义如下:

参数本例实参作用
thread&th2_ptr静态线程控制块的地址
name"th2"线程名称,用于对象管理和调试
entryth2_printf线程入口函数地址
parameterNULL传给入口函数的参数;本节不传参数
stack_startth2_stack用户提供的线程栈起始地址
stack_size1024栈空间大小,单位为字节
priority21线程优先级;RT-Thread 中数字越小,优先级越高
tick10同优先级线程轮转时的初始时间片,单位为系统 tick

视频前半段先将 th2 的优先级设为 20,以便与 th1 同优先级观察运行现象;最后的调度实验把它改成了 21,所以上方最终代码保留 21。

rt_thread_init() 只负责初始化静态线程对象,成功后线程仍处于初始状态。还必须调用 rt_thread_startup(&th2_ptr),线程才会进入就绪队列并参与调度。

它的返回类型是 rt_err_t。配套内核把 rt_err_t 定义为有符号的 rt_base_t,因为失败状态可能是负值,所以不能用无符号类型接收。本节视频使用 int ret,在当前 Cortex-M3 工程中可以保存该状态码;从接口语义和可移植性看,实际项目通常更适合直接声明 rt_err_t ret,但这里为了忠实保留课堂源码不做替换。

本项目的 RT_ALIGN_SIZE 为 4,静态线程栈需要满足平台的对齐要求。课程代码原样使用全局数组 th2_stack;RT-Thread 官方示例常使用 ALIGN(RT_ALIGN_SIZE) 显式声明栈对齐。这里仅说明该要求,不擅自修改视频源码。

1.2 线程打印示例

静态线程初始化成功的判断条件是 ret == RT_EOK。代码使用反向判断:

if (ret != RT_EOK)

只有失败时才输出错误日志并返回错误码。日志文本仍写着 rt_thread_create failed,但这里实际失败的接口是 rt_thread_init();这是课堂源码中的提示文字不够准确,并不代表代码又调用了一次动态创建接口。

th1、th2 和主线程都启动后,串口输出的先后次序由线程优先级、就绪队列、启动时机、阻塞时间和系统 tick 等因素共同决定。视频中多次复位看到某个线程先打印,不应概括成“概率性问题”:RTOS 调度器按规则选择线程,并不是随机抽取线程;只是应用程序不应把业务正确性建立在两条日志偶然呈现的先后顺序上。

本节还演示了线程结束接口的对应关系:

  • rt_thread_create() 动态创建的线程,用 rt_thread_delete() 删除;
  • rt_thread_init() 静态初始化的线程,用 rt_thread_detach() 脱离内核对象管理。

视频中的删除对象是动态线程 th1,因此在线程 th2 中调用 rt_thread_delete(th1_ptr) 是正确的。转录中将静态接口识别成了 “task”,正确名称是 detach。静态 TCB 和静态栈由用户提供,rt_thread_detach() 只解除内核对线程对象的管理,不会像动态对象那样释放这两块用户内存。

1.3 线程删除错误示例

删除实验最初写成了类似下面的逻辑:

count++;
if (count > 5)
    rt_thread_delete(th1_ptr);

当 count 第一次变为 6 时,th1 被删除;下一轮 count 变为 7,条件仍成立,于是代码再次把已经关闭、甚至可能已经被空闲线程回收的动态线程句柄交给 rt_thread_delete()。这是对失效线程对象的重复操作,会触发 RT-Thread 内部的参数/对象断言。视频显示的 thread.c 行号只对应当时使用的内核版本,换版本后行号可能改变,排错时应关注断言所在函数和条件,而不是死记第 538 行。

课程把条件改为 count == 5,保证删除语句只触发一次:

count++;
if (count == 5)
    rt_thread_delete(th1_ptr);

运行约 5 次循环后,th1 停止打印,只剩 th2 和主线程继续运行。课程后续为了继续演示调度,又把计数和删除代码全部注释,因此这些语句在顶部最终代码中也保持注释状态。

在正式项目中,还应检查 rt_thread_delete() 的返回值,并在成功删除后清除或重新管理相应句柄,避免再次使用失效对象。不能通过把判断从 > 改成 == 就推导出所有生命周期问题都已解决;本节修改只是修正这个循环中的重复删除。

1.4 函数错误信息示例

RT-Thread 的断言信息能够显示函数名和源码行号。本节据此演示了三个编译器预定义信息:

名称本节输出内容说明
__FILE__../applications/main.c当前源文件名或编译器记录的源文件路径
__FUNCTION__th1_printf、th2_printf当前函数名;这是 GCC 等编译器支持的扩展写法
__LINE__24、34 等宏所在位置的源码行号,代码增删后数字会变化

标准 C99 提供的函数名标识符是 __func__。本课程使用 GCC 工具链并在视频中写的是 __FUNCTION__,所以源码按视频原样保留,不擅自替换。

两个入口函数虽然使用同一个打印模板,但宏会在各自所在位置展开,因此能够自动打印不同的函数名和行号。rt_kprintf() 中 %s 对应文件名和函数名,%d 对应行号:

In ../applications/main.c,func th1_printf is running! the line is 24!
In ../applications/main.c,func th2_printf is running! the line is 34!

这种方法同样适用于普通裸机 C 工程,不依赖 RTOS;只要所用编译器支持相应标识符,并且工程已有可用的输出函数即可。

课程口头强调“线程里必须有延时或挂起”,应理解为工程设计原则,而不是 C 语法限制。线程入口当然可以写无限循环,但普通业务线程如果始终保持就绪、从不延时、不等待事件也不主动让出处理器,就会持续消耗 CPU;若它的优先级高于其他业务线程,低优先级线程将得不到运行机会。实际应用通常应让线程等待真实事件、信号量、消息队列或周期延时,而不是无意义地空转。

1.5 应用案例

1.5.1 例题: 线程打印示例

实验先让 th1 和 th2 使用相同优先级,并临时注释两个线程中的 rt_thread_mdelay(1000)。两个线程都一直处于就绪状态,RT-Thread 对同优先级线程采用时间片轮转,因此串口中能看到来自第 24 行和第 34 行的输出,证明两个入口函数都获得了运行机会。

因为线程高速循环打印,一个线程输出过程中可能发生调度切换,串口文本会密集甚至交错。这种现象说明输出次序受调度和输出速度影响,不代表两个线程在单核 STM32F103 上真正同时执行。单核 CPU 任一时刻只执行一个线程,多个线程是通过快速上下文切换形成并发效果。

1.5.2 例题: 线程运行示例

随后把 th1 的优先级设为 20,把 th2 设为 21。RT-Thread 的优先级数字越小越高,因此 th1 比 th2 优先级高。

当两个线程都没有延时或其他阻塞操作时,th1 的无限循环使它始终处于可运行状态。只要优先级 20 的 th1 一直就绪,调度器就不会选择优先级 21 的 th2,串口中只会出现 th1_printf 的信息。这里不是 th2 创建失败,而是严格优先级抢占调度的结果。不同优先级线程之间不能依靠时间片轮转来解决饥饿问题,时间片主要用于同优先级就绪线程之间的轮转。

1.5.3 例题: 线程切换运行示例

最后重新启用两个入口函数中的 rt_thread_mdelay(1000)。本项目 RT_TICK_PER_SECOND 为 1000,rt_thread_mdelay(1000) 会把 1000 ms 换算成系统 tick,再调用线程睡眠逻辑,使当前线程暂时离开就绪队列。

此时的调度过程可概括为:

  1. 优先级 20 的 th1 就绪时先运行,打印一行后调用延时并进入阻塞状态。
  2. th1 阻塞后,优先级 21 的 th2 成为可运行的最高优先级业务线程,于是获得 CPU,打印一行后也进入阻塞状态。
  3. 延时时间到达后,线程重新进入就绪队列,调度器再次根据优先级选择线程。

这就是 RTOS 线程延时与裸机忙等待延时的关键区别:线程延时期间,CPU 可以执行其他就绪线程;忙等待则是在当前执行流中反复消耗 CPU 时间。1000 ms 表示请求的阻塞时长,实际再次获得 CPU 的时刻还会受到 tick 边界和更高优先级线程运行时间影响,所以不应把它理解成绝对精确的周期定时器。

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