RT-Thread快速入门-线程管理(下)

知识的领域是无限的,我们的学习也是无限期的。这篇文章主要讲述RT-Thread快速入门-线程管理(下)相关的知识,希望能为你提供帮助。
上一篇主要介绍了 RT-Thread 线程管理相关的理论知识:
RT-Thread快速入门-线程管理
这篇重点介绍 RT-thread 提供的线程管理相关的接口函数,以及实战演示。
线程创建
在 RT-Thread 中,创建一个线程的方式有两种:

  • 动态创建方式,线程的栈和线程控制块由系统从动态内存堆上分配。
  • 静态创建方式,线程的栈和线程控制块由用户定义分配。
1. 动态创建线程
动态创建线程,用户不需要考虑线程栈和线程控制块空间分配的问题,全部由系统自动完成分配。用户只需要关心其他关键的线程属性即可。
RT-Thread 动态创建一个线程的接口函数为 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_tpriority, rt_uint32_t tick)

该函数的详细参数在上一篇文章中做过详细的解释,在此不再赘述。
其中关键的几个参数分别是:
  • 线程入口函数指针 entry,需要用户定义一个函数,创建线程的时候,将函数名放在这个参数位置。
  • 线程栈大小 stack_size,单位是字节。根据实际情况设置这个参数,后边会分析如何确定这个值。
  • 线程优先级 priority,根据线程需要完成任务的重要性来决定优先级值,值越小,优先级越高。
  • 时间片 tick,单位为 系统时钟节拍,如果有相同优先级的线程,才会用到此参数。
动态创建线程举例:
/* 线程入口函数 */ void thread_entry(void *parameter).../* 定义线程控制块指针 */ rt_thread_t tid = RT_NULL; /* 创建线程 */ tid = rt_thread_create("thread_test", thread_entry, RT_NULL, 512, 10, 5);

首先定义一个线程控制块指针(线程句柄),然后调用 rt_thread_create() 函数创建线程。
此线程的名字为“thread_test”;线程入口函数 thread_entry;入口函数的参数为 RT_NULL,无入口参数;线程栈的大小为 512 字节;线程优先级为 10;线程时间片为 5。
2. 静态创建线程
静态方式创建线程,需要用户考虑的东西多一点:线程控制块定义、线程栈空间申请、线程栈起始地址等。
静态创建线程分两步:
  • 用户定义线程控制块结构变量,申请线程栈内存空间。
  • 初始化线程控制块,即初始化线程。
线程控制块(线程句柄)定义可以通过如下方式完成,即定义 struct rt_thread 结构变量:
struct rt_threadthread_static;

线程栈可以通过定义数组的方式来分配,或者通过动态内存分配的方式来完成:
/* 数组方式确定线程栈,应该定义成全局数组 */ char thread_stack[1024]; /* 动态内存申请方式,确定线程栈 */ char *thread_stack = (char *)rt_malloc(1024);

其中 rt_malloc() 函数会在后面内存管理文章做详细讲解。
线程控制块和线程栈定义完成后,需要对其进行初始化。RT-Thread 提供了线程初始化函数接口 rt_thread_init(),其函数原型定义为:
rt_err_t rt_thread_init(struct rt_thread *thread, const char*name, void (*entry)(void *parameter), void*parameter, void*stack_start, rt_uint32_tstack_size, rt_uint8_tpriority, rt_uint32_ttick)

该函数的各个参数解释如下:
参数 描述
thread 线程句柄,由用户提供,指向线程控制块内存地址
name 线程名称
entry 线程入口函数
parameter 线程入口函数的参数
stack_start 线程栈起始地址
stack_size 线程栈大小,单位是字节。
priority 线程的优先级。
tick 线程的时间片大小。
函数执行成功,返回 RT_EOK;执行失败,则返回 -RT_EOK。
要注意,用户提供的栈首地址需要做系统对齐,例如 ARM 架构的 CPU 上需要做 4 字节对齐。
静态创建线程举例:
/* 线程栈起始地址做内存对齐 */ ALIGN(RT_ALIGN_SIZE) char thread_stack[1024]; /* 定义线程控制块 */ struct rt_thread thread; /* 线程入口函数 */ void thread_entry(void *parameter).../* 初始化线程控制块 */ rt_thread_init(& thread, "thread_test", thread_entry, RT_NULL, & thread_stack[0], sizeof(thread_stack), 10, 5);

首先定义线程栈以及线程控制块,然后对线程控制块进行初始化。
线程句柄为线程控制块thread 的地址 & thread;线程名称为 "thread_test";线程入口函数为 thread_entry;入口函数的参数为 RT_NULL;线程栈起始地址为定义的数组的起始地址;线程栈大小为数组的字节数;优先级为 10;时间片为 5。
线程关键参数确定
创建一个线程有几个关键的参数需要用户确定:
  • 线程栈大小
  • 线程优先级
  • 线程时间片
【RT-Thread快速入门-线程管理(下)】对于初学者来说,这几个参数的确定不好把握,或者说,不知道设置多大合适。
其实这些参数的确定,没有统一的标准,需要根据实际的应用,具体分析来做决定。
1. 线程栈大小的确定
在基于 RTOS 的程序设计中,每个线程(任务)都需要自己的栈空间,每个线程需要的栈,根据应用的不同,栈大小也会随之不同。
需要用到栈空间的内容如下:
  • 函数调用需要用到栈空间的项目为:函数局部变量、函数形参、函数返回地址、函数内部状态。
  • 线程切换的上下文。线程切换需要用到的寄存器需要入栈。
  • 任务执行过程,发生中断。寄存器需要入栈。
实际应用中将这些加起来,可以粗略得到栈的最小需求,但是计算很麻烦。在实际分配栈大小的时候,可以粗略计算一个值后,取其二倍,比较保险。
2. 线程优先级分配
在 RT-Thread 中,线程优先级数值越小,其优先级越高。空闲任务的优先级最低。
线程优先级的分配,没有具体的标准。一般是根据具体的应用情况来配置。
为了能够使得某项事件得到及时处理,可以将处理此事件的线程设置为较高优先级。比如,按键检测、触摸检测、串口数据处理等等。
而对于那些实时处理不是很高的线程,则可以配置较低优先级。比如,LED 闪烁、界面显示等等。
3. 线程时间片分配
具有相同优先级的线程调度,线程时间片分配的长,则该线程执行时间长。
可以根据实际应用情况,如果某个线程完成某项事务,耗时比较长,可以给其分配较大的时间片。耗时较短的线程,分配较小的时间片。
如果应用程序中,没有相同优先级的线程,则此参数不起作用。
线程睡眠
在 RTOS 中,如果需要延时等待一会儿,千万不能用普通的延时等待(CPU 空转),应该调用 RTOS 提供的延时等待函数。如果用普通的延时,那么 RTOS 失去了实时性,浪费了 CPU 资源。
RT-Thread 提供了系统函数,用于让当前线程延迟一段时间,在指定的时间结束后,重新运行线程。线程睡眠可以使用以下三个函数:
rt_err_t rt_thread_sleep(rt_tick_t tick); /* 睡眠时间,单位为 时钟节拍 */ rt_err_t rt_thread_delay(rt_tick_t tick); /* 延时,单位为 时钟节拍 */ rt_err_t rt_thread_mdelay(rt_int32_t ms); /* 单位为 毫秒 */

这三个函数的作用相同,调用它们可以使得当前线程进入挂起状态,并持续一段指定的时间。这个时间到达后,线程会被唤醒并再次进入就绪状态。
rt_thread_sleep/delay() 的参数 tick,单位为 1 个系统时钟节拍(OS tick)。
rt_thread_mdelay() 的参数 ms,单位为 1ms。
函数的返回值为 RT_EOK。
使得线程进入休眠,即调用这三个函数中的一个,也是让出 CPU 权限的一种方式,可以让低优先级的线程能够得到执行。
如果高优先级的线程没有让出 CPU 的操作,那么低优先级的线程永远得不到 CPU 执行权限,从而引发问题出现。
因此,高优先级线程,要么等待某项系统资源不可用而进入挂起状态,要么调用这三个睡眠函数进入挂起状态,从而给低优先级线程执行的机会。
线程创建示例
此处用于演示如何使用上面介绍的线程创建函数:
#include < rtthread.h> #define THREAD_PRIORITY 25 #define THREAD_STACK_SIZE 512 #define THREAD_TIMESLICE 5 static rt_thread_t tid1 = RT_NULL; /* 线程1的入口函数 */ static void thread1_entry(void *parameter)rt_uint32_t count = 0; while (1)/* 线程1采用低优先级运行,一直打印计数值 */ rt_kprintf("thread1 count: %d\\n", count ++); /* 延时 500ms */ rt_thread_mdelay(500); ALIGN(RT_ALIGN_SIZE) static char thread2_stack[1024]; static struct rt_thread thread2; /* 线程2入口 */ static void thread2_entry(void *param)rt_uint32_t count = 0; /* 线程2拥有较高的优先级,以抢占线程1而获得执行 */ for (count = 0; count < 10 ; count++)/* 线程2打印计数值 */ rt_kprintf("thread2 count: %d\\n", count); rt_kprintf("thread2 exit\\n"); /* 线程2运行结束后也将自动被系统脱离 */int main()/* 创建线程1,名称是thread1,入口是thread1_entry */ tid1 = rt_thread_create("thread1", thread1_entry, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE); /* 如果获得线程控制块,启动这个线程 */ if (tid1 != RT_NULL)rt_thread_startup(tid1); /* 初始化线程2,名称是thread2,入口是 thread2_entry */ rt_thread_init(& thread2, "thread2", thread2_entry, RT_NULL, & thread2_stack[0], sizeof(thread2_stack), THREAD_PRIORITY - 1, THREAD_TIMESLICE); rt_thread_startup(& thread2);

这个例子用两种方式创建线程:静态方式和动态方式。一个线程在运行完毕后自动被系统删除,另一个线程一直打印计数。
编译运行,结果如下所示
RT-Thread快速入门-线程管理(下)

文章图片

系统线程
系统线程是指由系统创建的线程,而用户线程是由用户程序调用线程创建函数创建的线程。RT-Thread 内核的系统线程有两个:
  • 空闲线程
  • 主线程
1. 空闲线程
空闲线程是优先级最低的线程,该线程永远处于就绪状态。当系统中没有其他就绪线程时,调度器会将 CPU
权限给空闲线程。空闲线程永远不能挂起。
RT-Thread 的空闲线程由特殊用途:
  • 空闲线程会回收被删除线程的资源。
  • 可以设置空闲线程钩子函数,在空闲线程中调用。
2. 主线程
系统启动时,会自动创建 main 线程,其入口函数为 main_thread_entry(),用户的应用程序入口函数 main() 就是从这里开始。
RT-Thread 系统启动过程,可以参考文章:RT-Thread快速入门-了解内核启动流程
系统调度器启动后,main 线程就开始运行,函数调用过程如下图所示:
RT-Thread快速入门-线程管理(下)

文章图片

用户可以在 main() 函数中添加自己的应用程序代码。
线程其他管理函数
在此列出 RT-Thread 提供的其他线程管理函数接口,初学者可以作为了解即可。如果要详细学习,可以查看官方的编程手册。
1.删除线程
用 rt_thread_create() 创建出来的线程,当不需要使用时,可以使用下面的函数接口把它完全删除掉:
rt_err_t rt_thread_delete(rt_thread_t thread);

函数的参数 thread 为线程控制块指针。
此函数的作用是,把线程对象从线程队列中删除,释放线程占用的堆空间,并把相应的线程状态更改为 RT_THREAD_CLOSE 状态。
对于用 rt_thread_init() 初始化的线程,使用 rt_thread_detach() 将使线程对象在线程队列和内核对
象管理器中被脱离。线程脱离函数如下:
rt_err_t rt_thread_detach (rt_thread_t thread);

线程本身不会调用这两个函数,应该是其他线程调用,用于删除某个线程。
2. 获得当前运行线程
RT-Thread 提供了函数接口,用于查询当前正在执行的线程句柄:
rt_thread_t rt_thread_self(void);

该函数的返回值是,线程控制块指针(线程句柄)。
调用失败,则返回 RT_NULL,说明系统调度器还未启动。
3. 线程让出处理器
处于运行状态的线程可以主动让出 CPU 的使用权限,通过调用如下函数:
rt_err_t rt_thread_yield(void);

调用此函数后,当前线程首先把自己从它所在的就绪优先级线程队列中删除,然后把自己挂到这个优先级队列链表的尾部,然后激活调度器进行线程上下文切换(如果当前优先级只有这一个线程,则这个线程继续执行,不进行上下文切换动作)。
4. 挂起和恢复线程
线程挂起的函数接口如下:
rt_err_t rt_thread_suspend (rt_thread_t thread);

参数 thread 为线程句柄(线程控制块指针)。
线程挂起成功,返回 RT_OK;挂起失败,则返回 -RT_ERROR。
注意,通常不应该使用这个函数来挂起线程本身。
恢复一个挂起的线程,就是让它重新进入就绪状态,并将线程放入系统的就绪队列中。使得线程恢复的函数接口为:
rt_err_t rt_thread_resume (rt_thread_t thread);

5. 控制线程
当需要堆一个线程进行其他控制时,可以调用如下函数接口:
rt_err_t rt_thread_control(rt_thread_t thread, rt_uint8_t cmd, void* arg);

参数 thread 为线程句柄;参数 cmd 为控制指令;arg 为控制指令参数。
返回 RT_EOK,表示执行成功。返回 -RT_ERROR,表示执行失败。
指示控制命令 cmd 当前支持的命令如下:
  • RT_THREAD_CTRL_CHANGE_PRIORITY,动态更改线程优先级。
  • RT_THREAD_CTRL_STARTUP,开始运行一个线程。
  • RT_THREAD_CTRL_CLOSE,关闭一个线程。
  • RT_THREAD_CTRL_BIND_CPU,绑定线程到某个 CPU。
6. 设置和删除空闲钩子函数
RT-Thread 提供函数接口设置空闲钩子函数:
rt_err_t rt_thread_idle_sethook(void (*hook)(void)); rt_err_t rt_thread_idle_delhook(void (*hook)(void));

钩子函数在空闲线程中自动运行,可以在钩子函数中做一些其他事情,比如系统指示灯闪烁。
空闲线程必须永远为就绪态,因此钩子函数不能调用能挂起线程的函数。
小结
至此,RT-Thread 线程管理相关的内容学习完毕。这两篇文章,讲解了 RT-Thread 线程相关的理论知识,以及提供的系统函数接口。
并结合实验演示线程创建和线程延时的用法。其他线程管理简单进行了介绍。
对于入门来说,了解线程基础知识后,能够使用线程创建函数的使用即可。
深入学习的话,可以参考官方编程手册,详细学习线程管理其他的函数接口。
OK,今天先到这,下次继续。加油~

    推荐阅读