zsky · 2022年02月16日

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

首发,公众号【一起学嵌入式】,RTOS、Linux、C

上一篇主要介绍了 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_t  priority,
                             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_thread  thread_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_t       stack_size,
                        rt_uint8_t        priority,
                        rt_uint32_t       tick)

该函数的各个参数解释如下:

参数描述
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。

线程关键参数确定

创建一个线程有几个关键的参数需要用户确定:

  • 线程栈大小
  • 线程优先级
  • 线程时间片

对于初学者来说,这几个参数的确定不好把握,或者说,不知道设置多大合适。

其实这些参数的确定,没有统一的标准,需要根据实际的应用,具体分析来做决定。

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);
}

这个例子用两种方式创建线程:静态方式和动态方式。一个线程在运行完毕后自动被系统删除,另一个线程一直打印计数。

编译运行,结果如下所示

image-20220116223120736.png

系统线程

系统线程是指由系统创建的线程,而用户线程是由用户程序调用线程创建函数创建的线程。RT-Thread 内核的系统线程有两个:

  • 空闲线程
  • 主线程

1. 空闲线程

空闲线程是优先级最低的线程,该线程永远处于就绪状态。当系统中没有其他就绪线程时,调度器会将 CPU

权限给空闲线程。空闲线程永远不能挂起。

RT-Thread 的空闲线程由特殊用途:

  • 空闲线程会回收被删除线程的资源。
  • 可以设置空闲线程钩子函数,在空闲线程中调用。

2. 主线程

系统启动时,会自动创建 main 线程,其入口函数为 main_thread_entry(),用户的应用程序入口函数 main() 就是从这里开始。

RT-Thread 系统启动过程,可以参考文章:RT-Thread快速入门-了解内核启动流程

系统调度器启动后,main 线程就开始运行,函数调用过程如下图所示:

image-20220116232241018.png

用户可以在 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,今天先到这,下次继续。加油~

公众号【一起学嵌入式】,一起学习、一起成长
推荐阅读
关注数
2392
内容数
31
公众号【一起学嵌入式】专注嵌入式软件技术分享,RTOS、Linux、C/C++,一起学习,一起进步。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息