第三章节:深入浅出——RT_Thread线程创建与使用

1. 线程基础概念回顾 

  • 线程与进程的区别

  1. 进程:是操作系统进行资源分配和调度的基本单位。它是一个具有独立功能的程序关于某个数据集合的一次运行活动,是系统进行资源分配和调度的一个独立单位。
  2. 线程:是进程的执行单元,是进程内一个相对独立的、可调度的执行流,是CPU调度和分派的基本单位。
  • 线程的组成:

  1. RT-Thread 中,线程是 RT-Thread 中最基本的调度单位,使用 rt_thread 结构体表

    示线程。

    rt_thread 描述了一个线程执行的运行环境,也描述了这个线程所处的优先等级。

    系统中总共存在两类线程,分别是系统线程和用户线程

    系统线程由 RT-Thread 内核创建

    用户线程由用户应用程序创建

  2. 这两类线程都会从内核对象容器中分配线程对象,如下图所示。
    每个线程由三部分组成:线程控制块 (rt_thread 结构体 ) 、线程栈和入口函数
  • 线程的状态转换

  1. 线程运行的过程中,一个时间内只允许一个线程在处理器中运行,从运行的过程上划
    分,线程有多种不同的运行状态,如运行态,非运行态等。在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)
  • 线程调度

  1. 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 优先级相同,因此不存在抢占
  • t1thread1 进入运行态,一直运行直到 t2;这个时间长度就是 thread1
  • 设置的时间片长度,15 Tick 时钟节拍
  • t2thread2 进入运行态,一直运行直到 t3;这个时间长度就是 thread2
  • 设置的时间片长度,15 Tick 时钟节拍
  • t3thread1 重新进入运行态,如此交替

 

 示例 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及空闲线程。

结语

在结束本章的学习后,我想对读者说:

  • 请务必动手实践本章所学的知识,只有通过实际编码,才能真正理解和掌握线程的创建与使用。
  • 在实践中不断探索,尝试解决实际问题,这将帮助你更好地理解并发编程的复杂性和挑战。

### RT-Thread创建线程并传递参数 在 RT-Thread 操作系统中,可以通过 `rt_thread_create` 函数来创建一个新的线程,并可以向该线程传递参数。下面具体介绍这一过程。 #### 创建线程函数原型 创建线程使用的 API 是 `rt_thread_t rt_thread_create(const char* name, void (*thread_entry)(void *parameter), void *parameter, int stack_size, int priority, int tick)`[^1]。此接口允许指定线程名称、入口函数及其参数、栈大小、优先级以及时间片长度。 #### 参数解释 - **name**: 线程的名字,便于调试和识别。 - **thread_entry**:线程执行的起始地址,即线程主体函数指针。 - **parameter**:线程传递的数据,作为启动时传给线程主体函数的唯一参数。 - **stack_size**: 设置分配给线程堆栈的空间大小(单位字节),应根据实际需求合理设置。 - **priority**: 定义线程运行优先级,数值越低表示优先级别越高。 - **tick**: 时间片数量,决定了调度器多久切换一次当前正在运行的任务到其他等待处理的任务上。 #### 示例代码展示 以下是使用 C 语言编写的简单例子,展示了如何创建带有参数的新线程: ```c #include "rtthread.h" // 定义要在线程内调用的工作函数 static void thread_entry(void *param) { while (1) { // 打印接收到的消息 rt_kprintf("Received parameter: %s\n", (char *)param); // 延迟一段时间再继续循环 rt_thread_mdelay(1000); } } int main(void) { const char message[] = "Hello from new thread!"; // 使用 rt_thread_create 来实例化新的线程对象 struct rt_thread *new_thread; new_thread = rt_thread_create( "mythread", thread_entry, (void*)message, 1024, 10, 20); if(new_thread != RT_NULL){ // 如果成功创建,则启动这个线程 rt_thread_startup(new_thread); } return 0; } ``` 在这个例子中,通过 `main()` 函数内的 `rt_thread_create` 调用来建立名为 `"mythread"` 的新线程,并把字符串 `"Hello from new thread!"` 当作参数传递给了它。每当新线程被激活时就会打印这条消息。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值