动态创建线程

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

250.2.1动态创建线程

#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;                          // 定义线程句柄;它将保存指向线程控制块的指针。
void th1_printf(void *parameter)              // 定义 th1 的入口函数;本例没有使用 parameter。
{                                             // 线程入口函数开始。
    while (1)                                 // 线程持续运行,每轮输出后主动延时。
    {                                         // 无限循环开始。
        rt_kprintf("th1 is running!\r\n");   // 通过 RT-Thread 内核打印接口输出线程运行信息。
        rt_thread_mdelay(1000);                // 让当前线程阻塞约 1000 ms,期间 CPU 可运行其他就绪线程。
    }                                         // 结束本轮循环,随后再次判断 while 条件。
}                                             // th1 线程入口函数结束。
int main(void)                                // RT-Thread 初始化完成后,主线程从 main 开始执行。
{                                             // main 函数开始。
    int count = 1;                            // 作为主循环的课程示例计数器。
    th1_ptr = rt_thread_create("th1",         // 动态创建名为 th1 的线程,并保存返回的句柄。
                               th1_printf,    // 指定线程入口函数。
                               NULL,          // 本例不向入口函数传递参数。
                               1024,          // 请求为该线程分配 1024 字节栈空间。
                               20,            // 设置优先级为 20;RT-Thread 的数字越小优先级越高。
                               10);           // 设置同优先级线程间轮转的初始时间片为 10 个 tick。
    if (th1_ptr == RT_NULL)                   // 句柄为空表示动态创建失败。
    {                                         // 创建失败分支开始。
        LOG_E("th1_printf rt_thread_create failed!\r\n"); // 输出错误级别日志。
        return -RT_ENOMEM;                    // 按课程源码返回负的“内存不足”错误码。
    }                                         // 创建失败分支结束。
    rt_thread_startup(th1_ptr);               // 将已初始化的 th1 放入就绪队列并触发一次调度。
    while (count++)                           // 按课程源码持续运行主线程循环。
    {                                         // 主线程循环开始。
        LOG_D("Hello RT-Thread!");            // 输出主线程的调试日志。
        rt_thread_mdelay(1000);                // 让主线程阻塞约 1000 ms,给其他就绪线程运行机会。
    }                                         // 主线程循环结束。
    return RT_EOK;                            // 理论上返回成功;正常运行时无法执行到这里。
}                                             // main 函数结束。

一、动态创建线程

本课在 RT-Thread 主线程中调用 rt_thread_create() 创建 th1,再调用 rt_thread_startup() 启动它。主线程和 th1 都每隔约 1 s 输出一次,便于观察调度现象。

  • 动态创建:线程创建时,内核从堆中分配线程控制块和线程栈。线程退出后,由系统回收动态对象的资源。
  • 静态创建:程序先提供 struct rt_thread 对象和栈数组,运行时再调用 rt_thread_init() 初始化。“静态”描述的是存储空间由调用者预先准备,不是“编译时已经创建好线程”。
  • 启动:创建或初始化完成后,线程仍处于初始状态;还要调用 rt_thread_startup() 才会进入就绪队列。
  • 常见状态:初始、就绪、运行、挂起/阻塞和关闭。延时、等待 IPC 等操作会让线程暂时离开就绪队列。

1.1 线程的创建

1.1.1 线程的创建

线程不只是一个函数。入口函数只是线程执行的起点;一个可被 RTOS 调度的线程还需要:

  • 线程控制块(Thread Control Block,TCB),保存状态、优先级、时间片、栈指针等信息;
  • 独立的线程栈,用于函数调用、局部变量和上下文保存;
  • 入口函数及可选参数;
  • 调度器管理所需的队列节点和时钟信息。

在本课的单核 STM32F103 上,任一时刻只有一个线程占用 CPU。调度器在线程阻塞、时间片到期或更高优先级线程就绪时切换上下文,从而实现并发。rt_thread_mdelay() 会让当前线程进入延时阻塞状态,所以不会在等待的 1 s 内空转浪费 CPU。

裸机的主循环常用轮询顺次执行多项工作;RTOS 则由调度器在多个独立上下文之间选择就绪线程。RTOS 是一类对响应时间有可预期性要求的操作系统;Windows、桌面 Linux、Android 和 iOS 通常不应直接作为硬实时 RTOS 的例子。

1.1.2 动态创建线程

rt_thread_create() 的接口原型可概括为:

rt_thread_t rt_thread_create(const char *name,             // 线程名,用于标识和调试。
                             void (*entry)(void *parameter),// 线程入口函数指针。
                             void *parameter,               // 传给入口函数的不透明指针。
                             rt_uint32_t stack_size,        // 线程栈字节数。
                             rt_uint8_t priority,           // 线程优先级。
                             rt_uint32_t tick);             // 同优先级时的初始时间片,单位为系统 tick。

配套内核源码显示,该函数先通过 rt_object_allocate() 分配线程对象,再通过 RT_KERNEL_MALLOC(stack_size) 分配栈,然后由内部的 _thread_init() 填写入口、参数、栈、优先级和时间片等字段。线程对象或栈任一分配失败,函数都会返回 RT_NULL。

1.1.2.1 例题: 动态创建线程

本例六个实参的含义如下:

  1. "th1":线程对象名。项目中 RT_NAME_MAX 为 8,名称存储长度受该配置限制,并非可以无限长。
  2. th1_printf:入口函数地址。调度器首次切换到该线程时,从这个函数开始执行。
  3. NULL:不传递参数。该形参类型是 void *,可以指向任意类型的对象,但所指对象必须在线程使用它时仍然有效。
  4. 1024:栈大小为 1024 字节。这只是本例取值,不是所有线程的固定或推荐值。应结合函数调用深度、局部变量、库函数耗栈和中断/架构设计确定,并在运行中查看栈余量。
  5. 20:优先级。本项目配置 RT_THREAD_PRIORITY_MAX 为 32,可用优先级是 0~31,数字越小优先级越高。主线程优先级是 10,因此 th1 的 20 低于主线程;当主线程调用 rt_thread_mdelay() 后,th1 才有机会运行。
  6. 10:时间片是 10 个系统 tick。本项目 RT_TICK_PER_SECOND 为 1000,所以 1 tick 是 1 ms,10 tick 是 10 ms。原笔记中“每个 tick 为 10 ms”不正确。时间片主要用于同优先级就绪线程的轮转;线程可能在用完时间片前阻塞,也可能被更高优先级线程抢占,因此不能把两个不同时间片的线程简化成永远严格交替运行固定毫秒数。

rt_thread_create() 只完成分配和初始化。配套源码中,rt_thread_startup() 检查线程是初始状态,调用 rt_thread_resume() 将其放入就绪队列,随后调用 rt_schedule()。所以漏掉启动调用时,创建出的线程不会正常参与调度。

程序运行时,主线程的 Hello RT-Thread! 和 th1 的 th1 is running! 都大约每秒出现一次。两个延时的起点、时钟 tick 和串口输出耗时会影响先后顺序,不应依赖两行日志永远按某一顺序出现。

代码中的 while (count++) 是课程当时的原始写法,因此本笔记保留。但对有符号 int 不断自增最终会溢出,C 语言规定有符号整数溢出是未定义行为。实际的永久循环应直接表达为 while (1);这里不改动仅是为了忠实保留本课源码。

1.1.2.2 PCB百度搜索

PCB 是多义缩写。硬件语境中它通常是 Printed Circuit Board(印刷电路板);通用操作系统语境中它可表示 Process Control Block(进程控制块)。搜索时应加上“操作系统”、“RTOS”或具体系统名作为上下文。

1.1.2.3 PCB

本课对象是“线程”,更准确的术语是 TCB(线程控制块)。RT-Thread 中的实体类型是 struct rt_thread,rt_thread_t 则定义为 struct rt_thread *,即指向 TCB 的句柄。不应把这个指针本身称为 PCB,也不应把“控制块”和“函数返回值”视为同一概念。

rt_thread_t th1_ptr; 只为一个指针变量留出存储位置,此时并没有分配 TCB 或线程栈。真正的动态分配发生在 rt_thread_create() 内部。

创建失败时返回 RT_NULL,本课随后输出 LOG_E 并从 main() 返回 -RT_ENOMEM。这里的失败可能来自 TCB 对象分配,也可能来自栈分配。RT_EOK 是 0,但“0 成功、负数失败”是这类状态码接口的约定,不是 C 语言中所有函数的通用规则:rt_thread_create() 本身返回指针,成功与否应通过是否为空判断。

RT_ENOMEM 的数值还受 RT-Thread 版本和 libc 配置影响:配置 POSIX 兼容错误码时它可映射到 libc 的 ENOMEM,未启用时内核可自行定义数值。因此应在代码中使用宏,不要记成永远固定的“负 12”。

1.1.2.4 返回结构体指针

rt_thread_create() 成功时返回的是动态 TCB 的地址。句柄让后续 API 能找到对应线程,例如 rt_thread_startup(th1_ptr)。内核函数返回后,TCB 和栈仍位于堆中,不会像普通函数的局部自动变量那样随调用结束失效。

应区分三件事:th1_ptr 是指针变量,struct rt_thread 是内核管理的 TCB,1024 字节栈是另一块动态分配的内存。三者相关,但并不是同一块空间。