《嵌入式系统 – 玩转ART-Pi开发板》第4章 API-Pi使用多线程

4.1 线程简介

在讲解多线程之前,我们要搞清楚什么是多线程?为何要使用多线程?大家在刚接触到单片机,跑裸机的时候,程序执行的顺序都是自上而下的运行的,然而随着代码量的增加,执行单一任务的效率会非常差。就好比你以前都是先做完语文作业,再做数学作业,然后再做英语作业,那有没有一种方法,类似于分身术一般,在做语文作业的同时,把数学作业和英语作业一起做了。这就是多任务的实现。多任务,打破了原来从上而下的规规矩矩的执行方式。能帮助你在同一时间执行多种任务。因此在多线程操作系统中,需要开发人员把一个复杂的应用分解成多个小的、可调度的、序列化的程序单元,当合理地划分任务并正确地执行时,这种设计能够让系统满足实时系统的性能及时间的要求。

在 RT-Thread 中,与上述子任务对应的程序实体就是线程,线程是实现任务的载体,它是 RT-Thread 中最基本的调度单位,它描述了一个任务执行的运行环境,也描述了这个任务所处的优先等级,重要的任务可设置相对较高的优先级,非重要的任务可以设置较低的优先级,不同的任务还可以设置相同的优先级,轮流运行。

当线程运行时,它会认为自己是以独占 CPU 的方式在运行,线程执行时的运行环境称为上下文,具体来说就是各个变量和数据,包括所有的寄存器变量、堆栈、内存信息等

简单来说,多线程就是把复杂的单任务拆分成多个子任务,然后让子任务同时运行,值得注意的是,这里的“同时”并不是时间上的同时,而是所有任务在短时间内的快速切换,以此提高CPU利用率

4.2 RT-Thread线程的工作机制

RT-Thread 线程管理的主要功能是对线程进行管理和调度,系统中总共存在两类线程,分别是系统线程和用户线程,系统线程是由 RT-Thread 内核创建的线程,用户线程是由应用程序创建的线程,这两类线程都会从内核对象容器中分配线程对象,当线程被删除时,也会被从对象容器中删除,如图所示,每个线程都有重要的属性,如线程控制块、线程栈、入口函数等。

sM8crD.png

4.2.1线程控制块

在 RT-Thread 中,线程控制块由结构体 struct rt_thread 表示,线程控制块是操作系统用于管理线程的一个数据结构,它会存放线程的一些信息,例如优先级、线程名称、线程状态等,也包含线程与线程之间连接用的链表结构,线程等待事件集合等,详细定义如下:

/* 线程控制块 */
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_uint32_t number_mask;

    ......

    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;                      /* 用户数据 */
};

其中 init_priority 是线程创建时指定的线程优先级,在线程运行过程当中是不会被改变的(除非用户执行线程控制函数进行手动调整线程优先级)。cleanup 会在线程退出时,被空闲线程回调一次以执行用户设置的清理现场等工作。最后的一个成员 user_data 可由用户挂接一些数据信息到线程控制块中,以提供类似线程私有数据的实现。

4.2.2线程控重要属性

线程栈

RT-Thread 线程具有独立的栈,当进行线程切换时,会将当前线程的上下文存在栈中,当线程要恢复运行时,再从栈中读取上下文信息,进行恢复。

线程栈还用来存放函数中的局部变量:函数中的局部变量从线程栈空间中申请;函数中局部变量初始时从寄存器中分配(ARM 架构),当这个函数再调用另一个函数时,这些局部变量将放入栈中。

对于线程第一次运行,可以以手工的方式构造这个上下文来设置一些初始的环境:入口函数(PC 寄存器)、入口参数(R0 寄存器)、返回位置(LR 寄存器)、当前机器运行状态(CPSR 寄存器)

线程栈的增长方向是芯片构架密切相关的,RT-Thread 3.1.0 以前的版本,均只支持栈由高地址向低地址增长的方式,对于 ARM Cortex-M 架构,线程栈可构造如下图所示。

sMG3od.png

线程状态

线程运行的过程中,同一时间内只允许一个线程在处理器中运行,从运行的过程上划分,线程有多种不同的运行状态,如初始状态、挂起状态、就绪状态等。在 RT-Thread 中,线程包含五种状态,操作系统会自动根据它运行的情况来动态调整它的状态。 RT-Thread 中线程的五种状态,如下表所示:

状态 描述
初始状态 当线程刚开始创建还没开始运行时就处于初始状态;在初始状态下,线程不参与调度。此状态在 RT-Thread 中的宏定义为 RT_THREAD_INIT
就绪状态 在就绪状态下,线程按照优先级排队,等待被执行;一旦当前线程运行完毕让出处理器,操作系统会马上寻找最高优先级的就绪态线程运行。此状态在 RT-Thread 中的宏定义为 RT_THREAD_READY
运行状态 线程当前正在运行。在单核系统中,只有 rt_thread_self() 函数返回的线程处于运行状态;在多核系统中,可能就不止这一个线程处于运行状态。此状态在 RT-Thread 中的宏定义为 RT_THREAD_RUNNING
挂起状态 也称阻塞态。它可能因为资源不可用而挂起等待,或线程主动延时一段时间而挂起。在挂起状态下,线程不参与调度。此状态在 RT-Thread 中的宏定义为 RT_THREAD_SUSPEND
关闭状态 当线程运行结束时将处于关闭状态。关闭状态的线程不参与线程的调度。此状态在 RT-Thread 中的宏定义为 RT_THREAD_CLOSE

线程优先级

RT-Thread 线程的优先级是表示线程被调度的优先程度。每个线程都具有优先级,线程越重要,赋予的优先级就应越高,- 线程被调度的可能才会越大。

RT-Thread 最大支持 256 个线程优先级 (0\~255),数值越小的优先级越高,0 为最高优先级。在一些资源比较紧张的系统中,可以根据实际情况选择只支持 8 个或 32 个优先级的系统配置;对于 ARM Cortex-M 系列,普遍采用 32 个优先级。最低优先级默认分配给空闲线程使用,用户一般不使用。在系统中,当有比当前线程优先级更高的线程就绪时,当前线程将立刻被换出,高优先级线程抢占处理器运行。

时间片

每个线程都有时间片这个参数,但时间片仅对优先级相同的就绪态线程有效。系统对优先级相同的就绪态线程采用时间片轮转的调度方式进行调度时,时间片起到约束线程单次运行时长的作用,其单位是一个系统节拍(OS Tick)。假设有 2 个优先级相同的就绪态线程 A 与 B,A 线程的时间片设置为 10,B 线程的时间片设置为 5,那么当系统中不存在比 A 优先级高的就绪态线程时,系统会在 A、B 线程间来回切换执行,并且每次对 A 线程执行 10 个节拍的时长,对 B 线程执行 5 个节拍的时长,如下图。

线程的入口函数

线程控制块中的 entry 是线程的入口函数,它是线程实现预期功能的函数。线程的入口函数由用户设计实现,一般有以下两种代码形式:

无限循环模式:

在实时系统中,线程通常是被动式的:这个是由实时系统的特性所决定的,实时系统通常总是等待外界事件的发生,而后进行相应的服务:

void thread_entry(void* paramenter)
{
    while (1)
    {
    /* 等待事件的发生 */

    /* 对事件进行服务、进行处理 */
    }
}

线程看似没有什么限制程序执行的因素,似乎所有的操作都可以执行。但是作为一个实时系统,一个优先级明确的实时系统,如果一个线程中的程序陷入了死循环操作,那么比它优先级低的线程都将不能够得到执行。所以在实时操作系统中必须注意的一点就是:线程中不能陷入死循环操作,必须要有让出 CPU 使用权的动作,如循环中调用延时函数或者主动挂起。用户设计这种无线循环的线程的目的,就是为了让这个线程一直被系统循环调度运行,永不删除。

顺序执行或有限次循环模式:

如简单的顺序语句、do whlie() 或 for()循环等,此类线程不会循环或不会永久循环,可谓是 “一次性” 线程,一定会被执行完毕。在执行完毕后,线程将被系统自动删除。

static void thread_entry(void* parameter)
{
    /* 处理事务 #1 */
    …
    /* 处理事务 #2 */
    …
    /* 处理事务 #3 */
}

4.2.3线程状态切换

RT-Thread 提供一系列的操作系统调用接口,使得线程的状态在这五个状态之间来回切换。几种状态间的转换关系如下图所示:

sMGsFs.png

线程通过调用函数 rt_thread_create/init() 进入到初始状态(RT_THREAD_INIT);初始状态的线程通过调用函数 rt_thread_startup() 进入到就绪状态(RT_THREAD_READY);就绪状态的线程被调度器调度后进入运行状态(RT_THREAD_RUNNING);当处于运行状态的线程调用 rt_thread_delay(),rt_sem_take(),rt_mutex_take(),rt_mb_recv() 等函数或者获取不到资源时,将进入到挂起状态(RT_THREAD_SUSPEND);处于挂起状态的线程,如果等待超时依然未能获得资源或由于其他线程释放了资源,那么它将返回到就绪状态。挂起状态的线程,如果调用 rt_thread_delete/detach() 函数,将更改为关闭状态(RT_THREAD_CLOSE);而运行状态的线程,如果运行结束,就会在线程的最后部分执行 rt_thread_exit() 函数,将状态更改为关闭状态。

【注】RT-Thread 中,实际上线程并不存在运行状态,就绪状态和运行状态是等同的。

关于RT-Thread的更多讲解,请参看官方文档:
https://www.rt-thread.org/document/site/programming-manual/thread/thread/

4.3 RT-Thread线程应用

前文对线程的功能与工作机制进行了概念上的讲解。下图描述了线程的使用,包含:创建 / 初始化线程、启动线程、运行线程、删除 / 脱离线程。可以使用 rt_thread_create() 创建一个动态线程,也可使用 rt_thread_init() 初始化一个静态线程,动态线程与静态线程的区别是:动态线程是系统自动从动态内存堆上分配栈空间与线程句柄(初始化 heap 之后才能使用 create 创建动态线程),静态线程是由用户分配栈空间与线程句柄。

sMGWOU.png

下面给出应用示例。

4.3.1创建线程示例

这个例子创建一个动态线程初始化一个静态线程,一个线程在运行完毕后自动被系统删除,另一个线程一直打印计数。

task.c


/*Includes**********************************************************************/
#include "task.h"

define THREAD_PRIORITY 8

define THREAD_STACK_SIZE 256

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 ++);
    rt_thread_mdelay(1000);
}

}

ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[256];
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_thread_mdelay(1000);
}
rt_kprintf("thread2 exit\n");
/* 线程 2 运行结束后也将自动被系统脱离 */

}

/ 线程示例 /
int thread_sample(void)
{
/ 创建线程 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 - 2, THREAD_TIMESLICE);
rt_thread_startup(&thread2);

return 0;

}

/ 导出到 msh 命令列表中 /
MSH_CMD_EXPORT(thread_sample, thread sample);


>   task.h
```c
#ifndef _TASK_H_
#define _TASK_H_

#include <rtthread.h>
int thread_sample(void);

#endif</code></pre>
<p>将以上文件放入main.c目录下,将task.c加入工程即可,第一种就是通过Keil手动加载,还可通过RT-Thread 官方提供的 env 工具按照 rt-thread 开发的正常流程创建 MDK 工程。输入 <code>scons --target=mdk5</code> 命令生成 MDK 工程。</p>
<p><img src="https://s3.ax1x.com/2021/01/09/sMGok9.png" alt="sMGok9.png" /></p>
<p>打开Keil,查看文件是否导入成功:</p>
<p><img src="https://s3.ax1x.com/2021/01/09/sMGbSx.png" alt="sMGbSx.png" /></p>
<p>接下来就是编译下载,运行结果如下:</p>
<pre><code>  ___  ______  _____         ______  _   ______  _____  _____  _____ 
  / _ \ | ___ \|_   _|        | ___ \(_)  | ___ \/  _  \/  _  \|_   _|
 / /_\ \| |_/ /  | |   ______ | |_/ / _   | |_/ /| | | || | | |  | |  
 |  _  ||    /   | |  |______||  __/ | |  | ___ \| | | || | | |  | |  
 | | | || |\ \   | |          | |    | |  | |_/ /\ \_/ /\ \_/ /  | |  
 \_| |_/\_| \_|  \_/          \_|    |_|  \____/  \___/  \___/   \_/  

 Powered by RT-Thread.

 \ | /
- RT -     Thread Operating System
 / | \     4.0.3 build Jan  9 2021
 2006 - 2020 Copyright by rt-thread team
msh />thread_sample
thread_sample
thread1 count: 0
thread2 count: 0
thread2 count: 1
thread2 count: 2
thread2 count: 3
thread2 count: 4
thread2 count: 5
thread2 count: 6
thread2 count: 7
thread2 count: 8
thread2 count: 9
thread2 exit
msh />thread1 count: 1
thread1 count: 2
thread1 count: 3
thread1 count: 4
…</code></pre>
<p><img src="https://s3.ax1x.com/2021/01/09/sMGjmD.png" alt="sMGjmD.png" /></p>
<p>线程 2 计数到一定值会执行完毕,线程 2 被系统自动删除,计数停止。线程 1 一直打印计数。</p>
<p>【注】关于删除线程:大多数线程是循环执行的,无需删除;而能运行完毕的线程,RT-Thread 在线程运行完毕后,自动删除线程,在 rt_thread_exit() 里完成删除动作。用户只需要了解该接口的作用,不推荐使用该接口(可以由其他线程调用此接口或在定时器超时函数中调用此接口删除一个线程,但是这种使用非常少)。</p>
<p>上面例子看着很简单,但有很多小细节和需要注意,下面一一讲解。</p>
<p><strong>细节一:开启系统动态堆</strong>
在使用rt_thread_create()函数时,需要打开系统动态堆的宏定义,即 <strong>RT_USING_HEAP</strong>。</p>
<p><strong>细节二:堆栈空间大小设置</strong>
在这个点上很容易栽更头的,如果设置的堆栈空间不足,会导致程序挂掉。出现以下现象:</p>
<p><img src="https://s3.ax1x.com/2021/01/09/sMGxTH.png" alt="sMGxTH.png" /></p>
<p>以上错误的提示就是堆栈溢出,<strong>改下堆栈大小即可,大于等于256即可,但一般不超过4096</strong>。</p>
<p><strong>细节三:线程优先级问题</strong>
线程优先级的设置不正确也会导致整个系统挂掉,一般会出现以下现象:</p>
<p><img src="https://s3.ax1x.com/2021/01/09/sMJPpt.png" alt="sMJPpt.png" /></p>
<p>因为设置优先级的宏定义<strong>RT_THREAD_PRIORITY_MAX默认设置为32,如果你设置的优先级过大就出现以上问题。因此只要设置的优先级小于最大优先级即可。值得注意的是不同的BSP默认设置的最大优先级可能不同。</strong></p>
<p><img src="https://s3.ax1x.com/2021/01/09/sMJkX8.png" alt="sMJkX8.png" /></p>
<p><strong>细节四:线程时间片轮转调度问题</strong>
如果线程2中的延时注释:</p>
<pre><code class="language-c">rt_thread_mdelay(1000);</code></pre>
<p>我们再来看看实验现象:</p>
<p><img src="https://s3.ax1x.com/2021/01/09/sMJEnS.png" alt="sMJEnS.png" /></p>
<p>是不是很意外,线程2的优先级要高,但是两个线程却在交叉运行,这到底是为什么呢?要回答这个问题,我们看看前文的线程状态切换图,尽管线程2的优先级高,但是当使用了延时时,线程就挂起,那么优先级就不起作用,线程1就开始运行,当线程2休眠结束,就恢复运行状态,线程2有开始运行,线程1和线程2的休眠时间相同,因此是我们看到现象就是线程2和线程1交替运行。</p>
<p><img src="https://s3.ax1x.com/2021/01/09/sMJuhn.png" alt="sMJuhn.png" /></p>
<h3>4.3.2线程时间片轮转调度示例</h3>
<p>这个例子创建两个线程,在执行时会一直打印计数,代码如下。</p>
<blockquote>
<p>Task.c</p>
<pre><code class="language-c">
/*Includes**********************************************************************/
#include "task.h"
#define THREAD_STACK_SIZE   512
#define THREAD_PRIORITY     7
#define THREAD_TIMESLICE    10</code></pre>
</blockquote>
<p>/<em> 线程入口 </em>/
static void thread_entry(void* parameter)
{
rt_uint32_t value;
rt_uint32_t count = 0;</p>
<pre><code>value = (rt_uint32_t)parameter;
while (1)
{
    if(0 == (count % 5))
    {
        rt_kprintf("thread %d is running ,thread %d count = %d\n", value , value , count);

        if(count> 200)
            return;
    }
     count++;
 }</code></pre>
<p>}</p>
<p>int timeslice_sample(void)
{
rt_thread_t tid = RT_NULL;
/<em> 创建线程 1 </em>/
tid = rt_thread_create("thread1",
thread_entry, (void*)1,
THREAD_STACK_SIZE,
THREAD_PRIORITY, THREAD_TIMESLICE);
if (tid != RT_NULL)
rt_thread_startup(tid);</p>
<pre><code>/* 创建线程 2 */
tid = rt_thread_create("thread2",
                        thread_entry, (void*)2,
                        THREAD_STACK_SIZE,
                        THREAD_PRIORITY, THREAD_TIMESLICE-5);
if (tid != RT_NULL)
    rt_thread_startup(tid);
return 0;</code></pre>
<p>}</p>
<p>/<em> 导出到 msh 命令列表中 </em>/
MSH_CMD_EXPORT(timeslice_sample, timeslice sample);</p>
<pre><code>
>   task.h

```c
#ifndef _TASK_H_
#define _TASK_H_

#include <rtthread.h>

int timeslice_sample(void);

#endif

编译下载,运行结果如下:

   ___  ______  _____         ______  _   ______  _____  _____  _____ 
  / _ \ | ___ \|_   _|        | ___ \(_)  | ___ \/  _  \/  _  \|_   _|
 / /_\ \| |_/ /  | |   ______ | |_/ / _   | |_/ /| | | || | | |  | |  
 |  _  ||    /   | |  |______||  __/ | |  | ___ \| | | || | | |  | |  
 | | | || |\ \   | |          | |    | |  | |_/ /\ \_/ /\ \_/ /  | |  
 \_| |_/\_| \_|  \_/          \_|    |_|  \____/  \___/  \___/   \_/  

 Powered by RT-Thread.

 \ | /
- RT -     Thread Operating System
 / | \     4.0.3 build Jan  9 2021
 2006 - 2020 Copyright by rt-thread team
msh />timeslice_sample
timeslice_sample
thread 1 is running ,thread 1 count = 0
thread 1 is running ,thread 1 count = 5
thread 1 is running ,thread 1 count = 10
thread 1 is running ,thread 1 count = 15
thread 1 is running ,thread 1 count = 20
thread 1 is running ,thread 1 count = 25
thread 1 is running ,thread 1 count = 30
thread 1 is running ,thread 1 count = 35
thread 1 is running ,thread 1 count = 40
thread 1 is running ,thread 1 count = 45
thread 1 is running ,thread 1 count = 50
thread 1 is running ,thread 1 count = 55
thread 1 is running ,thread 1 count = 60
thread 1 is running ,thread 1 count = 65
thread 1 is running ,thread 1 count = 70
thread 1 is running ,thread 1 count = 75
thread 1 is running ,thread 1 count = 80
thread 1 is running ,thread 1 count = 85
thread 1 is running ,thread 1 count = 90
thread 1 is running ,thread 1 count = 95
thread 1 is running ,thread 1 count = 100
thread 1 is running ,thread 1 count = 105
thread 1 is running ,thread 1 count = 110
thread 1 is running ,thread 1 count = 115
thread 1 is running ,thread 1 count = 120
thread 1 is running ,thread 1 count = 125
thread 1 is running ,thread 1 count = 130
thread 1 is running ,thread 1 count = 135
thread 1 is running ,thread 1 count = 140
thread 1 is running ,thread 1 count = 145
thread 1 is running ,thread 1 count = 150
thread 1 is running ,thread 1 count = 155
thread 1 is running ,thread 1 count = 160
thread 1 is running ,thread 1 count = 165
thread 1 is running ,thread 1 count = 170
thread 1 is running ,thread 1 count = 175
thread 1 is running ,thread 1 count = 180
thread 1 is running ,thread 1 count = 185
thread 1 is running ,thread 1 count = 190
thread 1 is running ,thread 1 count = 195
thread 1 is running ,thread 1 count = 200
thread 1 is running ,thread 1 count = 205
thread 2 is running ,thread 2 count = 0
thread 2 is running ,thread 2 count = 5
thread 2 is running ,thread 2 count = 10
thread 2 is running ,thread 2 count = 15
thread 2 is running ,thread 2 count = 20
thread 2 is running ,thread 2 count = 25
thread 2 is running ,thread 2 count = 30
thread 2 is running ,thread 2 count = 35
thread 2 is running ,thread 2 count = 40
…
thread 2 is running ,thread 2 count = 205

由运行的计数结果可以看出,线程 2 的运行时间是线程 1 的一半。
当优先级相同时,会通过时间片来轮转,线程1和线程2唯一不同的是时间片不同,也就是rt_thread_create()函数的时间片tick参数。

4.3.3线程调度器钩子示例

在本实例之前,先搞清楚何为钩子函数。钩子函数是在一个事件触发的时候,在系统级捕获到了他,然后做一些操作。一段用以处理系统消息的程序。“钩子”就是在某个阶段给你一个做某些处理的机会。

钩子函数首先得是个函数,也就是在系统消息触发时被系统调用,而不是用户自己触发的。线程在进行调度切换时,会执行调度,我们可以设置一个调度器钩子,这样可以在线程切换时,做一些额外的事情,这个例子是在调度器钩子函数中打印线程间的切换信息,如下代码。

Task.c

/*Includes**********************************************************************/
#include "task.h"

#define THREAD_STACK_SIZE   256
#define THREAD_PRIORITY     7
#define THREAD_TIMESLICE    10

/* 针对每个线程的计数器 */
volatile rt_uint32_t count[2];

/* 线程 1、2 共用一个入口,但入口参数不同 */
static void thread_entry(void* parameter)
{
    rt_uint32_t value;

    value = (rt_uint32_t)parameter;
    while (1)
    {
        rt_kprintf("thread %d is running\n", value);
        rt_thread_mdelay(1000); // 延时一段时间
    }
}

static rt_thread_t tid1 = RT_NULL;
static rt_thread_t tid2 = RT_NULL;

static void hook_of_scheduler(struct rt_thread* from, struct rt_thread* to)
{
    rt_kprintf("from: %s -->  to: %s \n", from->name , to->name);
}

int scheduler_hook(void)
{
    /* 设置调度器钩子 */
    rt_scheduler_sethook(hook_of_scheduler);

    /* 创建线程 1 */
    tid1 = rt_thread_create("thread1",
                            thread_entry, (void*)1,
                            THREAD_STACK_SIZE,
                            THREAD_PRIORITY, THREAD_TIMESLICE);
    if (tid1 != RT_NULL)
        rt_thread_startup(tid1);

    /* 创建线程 2 */
    tid2 = rt_thread_create("thread2",
                            thread_entry, (void*)2,
                            THREAD_STACK_SIZE,
                            THREAD_PRIORITY,THREAD_TIMESLICE - 5);
    if (tid2 != RT_NULL)
        rt_thread_startup(tid2);
    return 0;
}

/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(scheduler_hook, scheduler_hook sample);

Task.h

#ifndef _TASK_H_
#define _TASK_H_

#include <rtthread.h>

int scheduler_hook(void);

#endif

编译下载,运行结果如下:

   ___  ______  _____         ______  _   ______  _____  _____  _____ 
  / _ \ | ___ \|_   _|        | ___ \(_)  | ___ \/  _  \/  _  \|_   _|
 / /_\ \| |_/ /  | |   ______ | |_/ / _   | |_/ /| | | || | | |  | |  
 |  _  ||    /   | |  |______||  __/ | |  | ___ \| | | || | | |  | |  
 | | | || |\ \   | |          | |    | |  | |_/ /\ \_/ /\ \_/ /  | |  
 \_| |_/\_| \_|  \_/          \_|    |_|  \____/  \___/  \___/   \_/  

 Powered by RT-Thread.

 \ | /
- RT -     Thread Operating System
 / | \     4.0.3 build Jan  9 2021
 2006 - 2020 Copyright by rt-thread team
msh />scheduler_hook
scheduler_hook
from: tshell -->  to: thread1 
thread 1 is running
from: thread1 -->  to: tshell 
from: tshell -->  to: thread2 
thread 2 is running
from: thread2 -->  to: tshell 
msh />from: tshell -->  to: tidle0 
from: tidle0 -->  to: main 
from: main -->  to: tidle0 
from: tidle0 -->  to: main 
from: main -->  to: tidle0 
from: tidle0 -->  to: thread1 
thread 1 is running
from: thread1 -->  to: tidle0 
from: tidle0 -->  to: thread2 
thread 2 is running
from: thread2 -->  to: tidle0 
from: tidle0 -->  to: main 
from: main -->  to: tidle0 
from: tidle0 -->  to: main 
from: main -->  to: tidle0 
from: tidle0 -->  to: thread1 
thread 1 is running
from: thread1 -->  to: tidle0 
from: tidle0 -->  to: thread2 
thread 2 is running
from: thread2 -->  to: tidle0 
from: tidle0 -->  to: main 
from: main -->  to: tidle0 
from: tidle0 -->  to: main 
from: main -->  to: tidle0 
…

由结果可以看出,对线程进行切换时,设置的调度器钩子函数是在正常工作的,一直在打印线程切换的信息,包含切换到空闲线程。

值得注意的是,使用钩子函数,需要打开RT_USING_HOOK宏定义

4.4 APT-Pi 使用线程总结

1.每个线程都有自己独立的线程栈空间,以此用来保存线程调度时上下文的信息,因此在分配线程栈空间的时候,要充分考虑栈的大小

2.RT-Thread的线程调度器是抢占式的,最高优先级的任务总能获得CPU的使用权,在任务设计的时候,要充分考虑好任务的优先级,同时设置的优先级不能超过最大优先级

3.在每个线程循环体中,不可一直占用CPU,这样会导致其他线程永远不能调度,需要通过延时操作让出CPU 资源

4.一般情况下,线程不需要手动删除,在线程运行完成后,系统会自动清理线程垃圾

Related posts

Leave a Comment