1. 线程基础概念回顾
-
线程与进程的区别
- 进程:是操作系统进行资源分配和调度的基本单位。它是一个具有独立功能的程序关于某个数据集合的一次运行活动,是系统进行资源分配和调度的一个独立单位。
- 线程:是进程的执行单元,是进程内一个相对独立的、可调度的执行流,是CPU调度和分派的基本单位。
-
线程的组成:
-
在 RT-Thread 中,线程是 RT-Thread 中最基本的调度单位,使用 rt_thread 结构体表
示线程。
rt_thread 描述了一个线程执行的运行环境,也描述了这个线程所处的优先等级。
系统中总共存在两类线程,分别是系统线程和用户线程
系统线程由 RT-Thread 内核创建
用户线程由用户应用程序创建
-
这两类线程都会从内核对象容器中分配线程对象,如下图所示。每个线程由三部分组成:线程控制块 (rt_thread 结构体 ) 、线程栈和入口函数

-
线程的状态转换
-
线程运行的过程中,一个时间内只允许一个线程在处理器中运行,从运行的过程上划分,线程有多种不同的运行状态,如运行态,非运行态等。在RT-Thread实时操作系统中,线程包含五种状态,操作系统会自动根据它运行的情况而动态调整它的状态。 RT-Thread中的五种线程状态如下所示:
-
RT-Thread实时操作系统提供一系列的操作系统调用接口,使得线程的状态在这五个状
态之间来回的变换。例如一个就绪态的线程由于申请一个资源(例如使用rt_sem_take),而
可能进入挂起态。又例如因为一个外部中断发生了,系统转入中断服务例程,在中断服务例
程中释放了相应的资源,导致把等待在这个资源上的高优先级线程唤醒,改变其状态为就绪
态,导致当前运行线程切换等等。
几种状态间的转换关系如 线程转换图 所示:
图 2.2: 线程转换图
线程通过调用函数rt_thread_create/init进入到初始状态(RT_THREAD_INIT);再通
过调用函数rt_thread_startup进入到就绪状态(RT_THREAD_READY);当处于就绪状态
的线程调用rt_thread_delay,rt_sem_take,rt_mb_recv等函数或由于获取不到资源时,
将进入到挂起状态(RT_THREAD_SUSPEND);处于挂起状态的线程,如果等待超时依然
未能获得资源或由于其他线程释放了资源,那么它将返回到就绪状态。挂起状态的线
程,如果调用rt_thread_delete/detach将更改为关闭状态(RT_THREAD_CLOSE);而运行
状态的线程,如果运行结束会在线程最后部分执行rt_thread_exit函数而更改为关闭状态
(RT_THREAD_CLOSE)
-
线程调度
-
RT-Thread中提供的线程调度器是基于优先级的全抢占式调度:在系统中除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统的其他部分都是可以抢占的,包括线程调度器自身。系统总共支持256个优先级(0 ~ 255,数值越小的优先级越高,0为最高优先级,255分配给空闲线程使用,一般用户不使用。在一些资源比较紧张的系统中,可以根据实际情况选择只支持8个或32个优先级的系统配置)。在系统中,当有比当前线程优先级更高的线程就绪时,当前线程将立刻被换出,高优先级线程抢占处理器运行。如图 线程就绪优先级队列 所示,在RT-Thread调度器的实现中,包含了一个共256个优先级队列的数组(如果系统最大支持32个优先级,那么这里将是一个包含了32个优先级队列的数组),每个数组元素中放置相同优先级链表的表头。这些相同优先级的列表形成一个双向环形链表,最低优先级线程链表一般只包含一个idle线程。
在优先级队列1#和2#中,可以看到三个线程:线程A、线程B和线程C。由于线程A、B
的优先级比线程C的高,所以此时线程C得不到运行,必须要等待优先级队列1#的中所有线
程(因为阻塞)都让出处理器后才能得到执行。
一个操作系统如果只是具备了高优先级任务能够“立即”获得处理器并得到执行的特
点,那么它仍然不算是实时操作系统。因为这个查找最高优先级线程的过程决定了调度时间
是否具有确定性,例如一个包含n个就绪任务的系统中,如果仅仅从头找到尾,那么这个时
间将直接和n相关,而下一个就绪线程抉择时间的长短将会极大的影响系统的实时性。当所
有就绪线程都链接在它们对应的优先级队列中时,抉择过程就将演变为在优先级数组中寻找
具有最高优先级线程的非空链表。RT-Thread内核中采用了基于位图的优先级算法(时间复
杂度O(1),即与就绪线程的多少无关),通过位图的定位快速的获得优先级最高的线程。
RT-Thread内核中也允许创建相同优先级的线程。相同优先级的线程采用时间片轮转方
式进行调度(也就是通常说的分时调度器),时间片轮转调度仅在当前系统中无更高优先级
就绪线程存在的情况下才有效。例如在 线程就绪优先级队列 图中,我们假设线程A和线程B
一次最大允许运行的时间片分别是10个时钟节拍和7个时钟节拍。那么线程B将在线程A的时
间片结束(10个时钟节拍)后才能运行,但如果中途线程A被挂起了,即线程A在运行的途
中,因为试图去持有不可用的资源,而导致线程状态从就绪状态更改为阻塞状态,那么线程
B会因为其优先级成为系统中就绪线程中最高的而马上运行。每个线程的时间片大小都可以
在初始化或创建这个线程时指定。
因为RT-Thread调度器的实现是采用优先级链表的方式,所以系统中的总线程数不受限
制,只和系统所能提供的内存资源相关。为了保证系统的实时性,系统尽最大可能地保证高
优先级的线程得以运行。线程调度的原则是一旦任务状态发生了改变,并且当前运行的线程
优先级小于优先级队列组中线程最高优先级时,立刻进行线程切换(除非当前系统处于中断
处理程序中或禁止线程切换的状态)。
2. 调度器相关接口
2.1.1 调度器初始化
在系统启动时需要执行调度器的初始化,以初始化系统调度器用到的一些全局变量。调
度器初始化可以调用下面的函数接口。
void rt_system_scheduler_init(void)
2.1.2 启动调度器
在系统完成初始化后切换到第一个线程,可以调用下面的函数接口。
void rt_system_scheduler_start(void);
在调用这个函数时,它会查找系统中优先级最高的就绪态线程,然后切换过去执行。另
外在调用这个函数前,必须先做idle线程的初始化,即保证系统至少能够找到一个就绪状态
的线程执行。此函数是永远不会返回的。
2.1.3 执行调度
让调度器执行一次线程的调度可通过下面的函数接口。
void rt_schedule(void);
调用这个函数后,系统会计算一次系统中就绪态的线程,如果存在比当前线程更高优先
级的线程时,系统将切换到高优先级的线程去。上层应用程序一般不需要调用这个函数。
2.1.4 设置调度器钩子
在整个系统的运行时,系统都处于线程运行、中断触发-响应中断、切换到其他线程,
甚至是线程间的切换过程中,或者说系统的上下文切换是系统中最普遍的事件。有时用户可
能会想知道在一个时刻发生了什么样的线程切换,可以通过调用下面的函数接口设置一个相
应的钩子函数。在系统线程切换时,这个钩子函数将被调用:
void rt_scheduler_sethook(void (*hook)(struct rt_thread* from, struct rt_thread* to));
这个函数用于把用户提供的hook函数设置到系统调度器钩子中,当系统进行上下文切
换时,这个hook函数将会被系统调用。
这个hook函数的声明如下:
void hook(struct rt_thread* from, struct rt_thread* to);
函数参数
参数:
描述
from
表示系统所要切换出的线程控制块指针;
to
表示系统所要切换到的线程控制块指针。
函数返回
无
• 注:请仔细编写你的钩子函数,稍有不慎将很可能导致整个系统运行不正常(在这个
钩子函数中,基本上不允许调用系统API,更不应该导致当前运行的上下文挂起)。
3. 线程的创建方法
3.1 1.线程控制块
线程控制块由结构体
struct rt_thread
表示,线程控制块是操作系统用于管理线程的一
个数据结构。
它存放线程的一些信息,例如优先级、线程名称、线程状态等,也包含线程与线程之
间连接用的链表结构,线程等待事件集合等。
它在
rtdef.h
中定义如下
/**
* Thread structure
*/
struct rt_thread
{
/* rt 对象 */
char name[RT_NAME_MAX]; /* 线程名字 */
rt_uint8_t type; /* 对象类型 */
rt_uint8_t flags; /* 标注位 */
rt_list_t list; /* 对象列表 */
rt_list_t tlist; /* 线程列表 */
/* 栈指针和入口指针 */
void *sp; /* 栈指针 */
void *entry; /* 入口函数指针*/
void *parameter; /* 参数 */
void *stack_addr; /* 栈地址指针 */
rt_uint32_t stack_size; /* 栈大小*/
/* 错误代码 */
rt_err_t error; /* 线程错误代码 */
rt_uint8_t stat; /* 线程状态 */
/* 优先级 */
rt_uint8_t current_priority; /* 当前优先级 */
rt_uint8_t init_priority; /* 初始优先级 */
…………
rt_ubase_t init_tick; /* 线程初始化计数值 */
rt_ubase_t remaining_tick; /* 线程剩余计数值 */
struct rt_timer thread_timer; /* 内置线程定时器 */
void (*cleanup)(struct rt_thread *tid); /* 线程退出清理函数 */
rt_uint32_t user_data; /* 用户私有数据*/
};
typedef struct rt_thread *rt_thread_t;
3.1.2.线程栈
在裸机系统中, 涉及局部变量、子函数调用或中断发生,就需要用到栈。
在
RTOS
系统中,每个线程运行时,也是普通的函数调用,也涉及局部变量、子函数
调用、中断,也要用到栈。
但不同于裸机系统,
RTOS
存在多个线程,每个线程是独立互不干扰的,因此需要为
每个线程都分配独立的栈空间,这就是线程栈。
可以使用两种方法提供线程栈:静态分配、动态分配。栈的大小通常由用户定义,如
下使用全局数组提供了一个静态栈,大小为
512
字节:
rt_uint32_t test_stack[512];
3.1.3入口函数
入口函数是线程要运行函数,由用户自行设计。
可分为无限循环模式和顺序执行模式。
无限循环模式:
void thread_entry(void* paramenter)
{
while (1)
{
/* 等待事件的发生 */
/* 对事件进行服务、进行处理 */
}
}
顺序执行模式:
static void thread_entry(void* parameter)
{
/* 处理事务 #1 */
…
/* 处理事务 #2 */
…
/* 处理事务 #3 */
}
使用这种模式时,线程不会一直循环,最后一定会执行完毕。
执行完毕后,线程将被系统自动删除。
4. 线程的使用及创建
4.1 线程的创建与启动
RT-Thread
提供两种线程的创建方式:
静态线程:使用
rt_thread_init()
初始化
动态线程:使用
rt_thread_create()
创建
区别:动态线程是系统自动从动态内存堆上分配栈空间与线程句柄,静态线程是由用
户分配栈空间与线程句柄
静态线程初始化函数如下
rt_err_t rt_thread_init(struct rt_thread* thread, //线程句柄
const char* name, //线程名字
void (*entry)(void* parameter), //入口函数
void* parameter, //入口函数参数
void* stack_start, //线程栈起始地址
rt_uint32_t stack_size, //栈大小
rt_uint8_t priority, //线程优先级
rt_uint32_t tick); //线程时间片大小
动态线程创建函数如下:
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); //线程时间片大小
创建线程后,还需要启动线程,才能让线程运行起来。
启动线程函数如下:
rt_err_t rt_thread_startup
(
rt_thread_t thread
);

4.2 线程的删除
创建线程时有
2
种方式,删除线程时也有对应的函数:
rt_err_t rt_thread_detach (rt_thread_t thread); // 删除使用 rt_thread_init()创建的线程
rt_err_t rt_thread_delete(rt_thread_t thread); // 删除使用 rt_thread_create()创建的线程
注意
- rt_thread_delete 并不是真正的删除线程,只是把线程状态状态改为 RT_THREAD_CLOSE。
- 真正的删除(释放线程控制块和线程栈),在下一次执行空闲线程时,由空 闲线程删除
- 线程本身不应调用 rt_thread_detach 脱离线程
5. 实际案例分析与代码演示
示例 1: 创建线程
代码为:
RT-Thread_01_create_task
使用两种方式分别创建两个线程。
线程
1
的代码:
/* 线程 1 的入口函数 */
static void thread1_entry(void *parameter)
{
const char *thread_name = "Thread1 run\r\n";
volatile rt_uint32_t cnt = 0;
/* 线程 1 */
while(1)
{
/* 打印线程 1 的信息 */
rt_kprintf(thread_name);
/* 延迟一会(比较简单粗暴) */
for( cnt = 0; cnt < 100000; cnt++ )
{
}
}
}
线程
2
的代码:
/* 线程 2 入口函数 */
static void thread2_entry(void *param)
{
const char *thread_name = "Thread2 run\r\n";
volatile rt_uint32_t cnt = 0;
/* 线程 2 */
while(1)
{
/* 打印线程 2 的信息 */
rt_kprintf(thread_name);
/* 延迟一会(比较简单粗暴) */
for( cnt = 0; cnt < 100000; cnt++ )
{
}
}
}
main
函数:
int main(void)
{
/* 初始化静态线程 1,名称是 Thread1,入口是 thread1_entry */
rt_thread_init(&thread1, //线程句柄
"thread1", //线程名字
thread1_entry, //入口函数
RT_NULL, //入口函数参数
&thread1_stack[0], //线程栈起始地址
sizeof(thread1_stack), //栈大小
THREAD_PRIORITY, //线程优先级
THREAD_TIMESLICE); //线程时间片大小
/* 启动线程 1 */
rt_thread_startup(&thread1);
/* 创建动态线程 2,名称是 thread2,入口是 thread2_entry*/
thread2 = rt_thread_create("thread2", //线程名字
thread2_entry, //入口函数
RT_NULL, //入口函数参数
THREAD_STACK_SIZE, //栈大小
THREAD_PRIORITY, //线程优先级
THREAD_TIMESLICE); //线程时间片大小
/* 判断创建结果,再启动线程 2 */
if (thread2 != RT_NULL)
rt_thread_startup(thread2);
return 0;
}
注意:
- 线程 1 是静态初始化,线程 2 是动态初始化
- 它们的优先级,时间片大小都设置一样
线程运行图:
- thread1 和 thread2 优先级相同,因此不存在抢占
- 在 t1,thread1 进入运行态,一直运行直到 t2;这个时间长度就是 thread1
- 设置的时间片长度,15 个 Tick 时钟节拍
- 在 t2,thread2 进入运行态,一直运行直到 t3;这个时间长度就是 thread2
- 设置的时间片长度,15 个 Tick 时钟节拍
- 在 t3,thread1 重新进入运行态,如此交替
示例 2: 使用线程参数
代码为:
RT-Thread_02_create_task_use_params
多个线程可以使用同一个函数,怎么体现它们的差别?
栈不同
创建线程时可以传入不同的参数
我们创建
2
个线程,使用同一个函数,代码如下:
/* 线程的入口函数 */
static void thread1_entry(void *parameter)
{
const char *thread_name = parameter;
volatile rt_uint32_t cnt = 0;
/* 线程 */
while(1)
{
/* 打印线程的信息 */
rt_kprintf(thread_name);
/* 延迟一会(比较简单粗暴) */
for( cnt = 0; cnt < 100000; cnt++ )
{
}
}
}
上述代码中的
thread_name
来自参数
parameter
,
parameter
来自哪里?创建线程时
传入的。
代码如下:
使用
rt_thread_init
和
rt_thread_create
分别创建线程时,传入不同的函数
参数
不同的线程,
parameter
不一样
static const char *thread1_name = "Thread1 run\r\n";
static const char *thread2_name = "Thread2 run\r\n";
int main(void)
{
/* 初始化静态线程 1,名称是 Thread1,入口是 thread1_entry */
rt_thread_init(&thread1, //线程句柄
"thread1", //线程名字
thread1_entry, //入口函数
(void *)thread1_name, //入口函数参数
&thread1_stack[0], //线程栈起始地址
sizeof(thread1_stack), //栈大小
THREAD_PRIORITY, //线程优先级
THREAD_TIMESLICE); //线程时间片大小
/* 启动线程 1 */
rt_thread_startup(&thread1);
/* 创建动态线程 2,名称是 thread2,入口也是 thread1_entry*/
thread2 = rt_thread_create("thread2", //线程名字
thread1_entry, //入口函数
(void *)thread2_name, //入口函数
参数
THREAD_STACK_SIZE, //栈大小
THREAD_PRIORITY, //线程优先级
THREAD_TIMESLICE); //线程时间片大小
/* 判断创建结果,再启动线程 2 */
if (thread2 != RT_NULL)
rt_thread_startup(thread2);
return 0;
}
5. 本章小结
- 掌握线程创建与使用的是关键点。
- 了解不同创建方法的适用场景 如静态创建及动态创建的区别及在内核中如何分布的(详细价将会在后面慢慢补充)。
- 下一章节将详细讲解线程优先级和tick及空闲线程。
结语
在结束本章的学习后,我想对读者说:
- 请务必动手实践本章所学的知识,只有通过实际编码,才能真正理解和掌握线程的创建与使用。
- 在实践中不断探索,尝试解决实际问题,这将帮助你更好地理解并发编程的复杂性和挑战。