RT-thread-2022夏令营-学习总结-第三天(线程同步)
前言:今天是夏令营的第二天,讲述的内容非常多,分多篇进行记录,此篇主要讲述在RT-thread线程同步的原理主要内容有信号量与互斥量的原理与使用。
一、线程同步(IPC)
线程同步指的就是多个线程按照一定的规矩来确定运行的先后顺序,实现有序的运行,如果不实现线程同步,那线程间将是乱序的,在进行数据通信的时候会产生混乱,无法实现线程间的协作。
用官方手册的一个例子来解释线程同步的重要性:
将传感器数据写入到共享内存块的接收线程 #1 和将传感器数据从共享内存块中读出的线程 #2 都会访问同一块内存。为了防止出现数据的差错,两个线程访问的动作必须是互斥进行的,应该是在一个线程对共享内存块操作完成后,才允许另一个线程去操作,这样,接收线程 #1 与显示线程 #2 才能正常配合,使此项工作正确地执行。
二、信号量
信号量是一种轻型的用于解决线程间同步问题的内核对象,线程可以获取或释放它,从而达到同步或互斥的目的。
如图:信号量有信号控制块、信号值、等待列表等
- 信号量控制块是操作系统用于管理信号量的一个数据结构用struct rt_semaphore 结构体表示;
- 信号量的值对应了信号量对象的实例数目、资源数目;
- 线程等待队列是当实列队列被线程申请完了值为零的时候,线程进入等待队列,等待可用的信号量实例。
1、二值信号量的使用场景
1.1线程同步
首先有两个线程#1与#2,然后我们创建一个信号量,信号值初始为零,线程#1先去获取信号量发现信号值为0,所以进行等待,当持有信号量的线程#2完成特定任务时释放信号量,线程#1拿到信号量将被唤醒完成线程#1的任务。在此过程中信号量可以理解为完标致,通过该标志来实现两个线程间的协作。
1.2中断与线程的同步
中断服务函数与线程间同步也可以使用信号量,当中断服务函数完成后需要幻想相应的线程时就可以释放信号量来唤醒被挂起的线程进行任务执行。
例如下图:中断接受UARTbuffer数据,等接受完毕后释放信号量来唤醒FinSH线程来对数据进行处理,实现线程间的有效协作。
2、计数型信号量的使用场景
计数型信号量一般使用在多线程协作的场景下,有消费者和生产者的角色存在,消费者take信号量生产者give信号量,如下图:当有4个线程形成消费者和生产者关系时引用信号量,就可以使线程间有序的去获取共享资源。
注:当多个线程被挂起的时,如果有信号量了先唤醒优先级高的线程,如果优先级相同就先唤醒等待时间旧的。
3、信号量的管理
信号量控制块中含有信号量相关的重要参数,在信号量各种状态间起到纽带的作用。信号量相关接口如下图所示,对一个信号量的操作包含:创建 / 初始化信号量、获取信号量、释放信号量、删除 / 脱离信号量。
3.1、创建动态信号量函数
当创建一个信号量时,内核首先创建一个信号量控制块,然后对该控制块进行基本的初始化工作。
注:信号量标志参数flag,决定了当多个线程挂起时的排队方式,有两种方式:RT_IPC_FLAG_FIFO(先进先出),RT_IPC_FLAG_PRIO(优先级等待)。
函数返回值: RT_NULL:创建失败 / 信号量的控制块指针:创建成功
rt_sem_t rt_sem_create(const char *name, //信号量名称
rt_uint32_t value, //信号量初始值
rt_uint8_t flag); //信号量标志
3.2、删除动态信号量函数
系统不再使用动态信号量时,可通过删除动态信号量以释放系统资源。
注:当有线程还在等待信号量时删除信号量,系统会唤醒在等待的线程再释放信号量的内存资源。
函数返回值:RT_EOK:删除成功。
rt_err_t rt_sem_delete(rt_sem_t sem); //sem:创建的信号量对象。
3.3、创建静态信号量函数
创建静态信号量就同创建静态函数一样,它的内存空间在编译时期就被编译器分配出来。
返回值:RT_EOK:初始化成功。
rt_err_t rt_sem_init(rt_sem_t sem, //信号量对象的句柄
const char *name, //信号量名称
rt_uint32_t value, //信号量初始值
rt_uint8_t flag); //信号量标志
3.4、删除静态信号量函数
当不再需要静态信号量时,可删除静态信号量,内核先唤醒所有挂在该信号量等待队列上的线程,然后将该信号量从内核对象管理器中脱离,原来挂起在信号量上的等待线程将获得-RT_ERROR 的返回值。
函数返回值:RT_EOK:删除成功。
rt_err_t rt_sem_detach(rt_sem_t sem); //信号量对象的句柄。
3.5、获取信号量函数
获取信号量函数作用与线程获得信号量,线程获得了信号量相应值-1,当信号量值为0时线程挂起,挂起前线程会按照函数参数time的情况选择直接返回、或挂起等待一段时间、或永久等待,直到其他线程或中断释放该信号量
函数返回值:RT_EOK成功获得信号量、-RT_ETIMEOUT 超时依然未获得信号量、-RT_ERROR 其他错误。
rt_err_t rt_sem_take (rt_sem_t sem, //信号量对象的句柄
rt_int32_t time); //指定的等待时间,单位是操作系统时钟节拍(OS Tick)
3.6、无等待获取信号量
当用户不想在申请的信号量上挂起线程进行等待时,可以使用无等待方式获取信号量,它的作用是和rt_sem_take(sem, 0) 一样的,即当线程申请的信号量资源实例不可用的时候,它不会等待在该信号量上,而是直接返回RT_ETIMEOUT。
返回值: RT_EOK:成功获得信号量、 RT_ETIMEOUT:获取失败。
rt_err_t rt_sem_trytake(rt_sem_t sem); //信号量对象的句柄
3.7、释放信号量函数
释放信号量可以唤醒挂起在该信号量上的线程,当信号量的值等于零时,并且有线程等待这个信号量时,释放信号量将唤醒等待在该信号量线程队列中的第一个线程,由它获取信号量,同时将把信号量的值加 1。
返回值:RT_EOK 成功释放信号量
rt_err_t rt_sem_release(rt_sem_t sem); //信号量对象的句柄
4、信号量应用示例(时间问题此部分截取RTT源码示例)
这是一个信号量使用例程,该例程创建了一个动态信号量,初始化两个线程,一个线程发送信号量,一个线程接收到信号量后,执行相应的操作。如下代码所示:
#define THREAD_PRIORITY 25
#define THREAD_TIMESLICE 5
/* 指向信号量的指针 */
static rt_sem_t dynamic_sem = RT_NULL;
ALIGN(RT_ALIGN_SIZE)
static char thread1_stack[1024];
static struct rt_thread thread1;
static void rt_thread1_entry(void *parameter)
{
static rt_uint8_t count = 0;
while(1)
{
if(count <= 100)
{
count++;
}
else
return;
/* count 每计数 10 次,就释放一次信号量 */
if(0 == (count % 10))
{
rt_kprintf("t1 release a dynamic semaphore.\n");
rt_sem_release(dynamic_sem);
}
}
}
ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;
static void rt_thread2_entry(void *parameter)
{
static rt_err_t result;
static rt_uint8_t number = 0;
while(1)
{
/* 永久方式等待信号量,获取到信号量,则执行 number 自加的操作 */
result = rt_sem_take(dynamic_sem, RT_WAITING_FOREVER);
if (result != RT_EOK)
{
rt_kprintf("t2 take a dynamic semaphore, failed.\n");
rt_sem_delete(dynamic_sem);
return;
}
else
{
number++;
rt_kprintf("t2 take a dynamic semaphore. number = %d\n" ,number);
}
}
}
/* 信号量示例的初始化 */
int semaphore_sample(void)
{
/* 创建一个动态信号量,初始值是 0 */
dynamic_sem = rt_sem_create("dsem", 0, RT_IPC_FLAG_PRIO);
if (dynamic_sem == RT_NULL)
{
rt_kprintf("create dynamic semaphore failed.\n");
return -1;
}
else
{
rt_kprintf("create done. dynamic semaphore value = 0.\n");
}
rt_thread_init(&thread1,
"thread1",
rt_thread1_entry,
RT_NULL,
&thread1_stack[0],
sizeof(thread1_stack),
THREAD_PRIORITY, THREAD_TIMESLICE);
rt_thread_startup(&thread1);
rt_thread_init(&thread2,
"thread2",
rt_thread2_entry,
RT_NULL,
&thread2_stack[0],
sizeof(thread2_stack),
THREAD_PRIORITY-1, THREAD_TIMESLICE);
rt_thread_startup(&thread2);
return 0;
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(semaphore_sample, semaphore sample);
运行结果:
从图中可以看出,线程1每计数10次产生一个信号量让线程2内number的值+1,最后线程1计数到100停止。
五、互斥量
互斥量简单来说就是互斥的信号量,他的特点就是能保证拥有互斥量的线程拥有最高运行权利。
举个栗子:在多任务系统中,任务A正在使用某个资源,还没用完的情况下任务B也来使用的话,就可能导致问题。比如对于串口,任务A正使用它来打印,在打印过程中任务B也来打印,A、B的信息混杂在一起。
1、互斥量的管理方式
互斥量控制块中含有互斥相关的重要参数,在互斥量功能的实现中起到重要的作用。互斥量相关接口如下图所示,对一个互斥量的操作包含:创建 / 初始化互斥量、获取互斥量、释放互斥量、删除 / 脱离互斥量。
1.1动态创建互斥量
创建一个互斥量时,内核首先创建一个互斥量控制块,然后完成对该控制块的初始化工作。创建互斥量使用下面的函数接口
返回值:互斥量句柄 创建成功、RT_NULL 创建失败
rt_mutex_t rt_mutex_create (const char* name, //互斥量的名称
rt_uint8_t flag); //都是按RT_IPC_FLAG_PRIO运行
1.2动态删除互斥量
当不再使用互斥量时,通过删除互斥量以释放系统资源,适用于动态创建的互斥量。删除互斥量使用下面的函数接口:
返回值:RT_EOK 删除成功
rt_err_t rt_mutex_delete (rt_mutex_t mutex); //互斥量对象的句柄
1.3创建静态互斥量
初始化互斥量使用下面的函数接口:
返回 值:RT_EOK 初始化成功
rt_err_t rt_mutex_init (rt_mutex_t mutex, //互斥量对象的句柄,它由用户提供,并指向互斥量对象的内存块
const char* name, //互斥量的名称
rt_uint8_t flag); //都是按RT_IPC_FLAG_PRIO运行
1.4删除静态互斥量
返回 值:RT_EOK 成功
rt_err_t rt_mutex_detach (rt_mutex_t mutex);//互斥量对象的句柄
1.5线程获取互斥量
获取了互斥量就有其所有权,互斥量只能同时被一个线程获取,获取互斥量使用下面的函数接口:
等待时间与信号量概念相同
返回值:RT_EOK 成功获得互斥量、RT_ETIMEOUT 超时、RT_ERROR 获取失败
rt_err_t rt_mutex_take (rt_mutex_t mutex,//互斥量对象的句柄
rt_int32_t time);//指定等待的时间
1.6、无等待获取互斥量
与无等待获取信号量概念相同
返回 值:RT_EOK 成功获得互斥量、RT_ETIMEOUT 获取失败
rt_err_t rt_mutex_trytake(rt_mutex_t mutex);//互斥量对象的句柄
1.7、释放互斥量
释放、让其他线程获取
返回值:RT_EOK 成功
rt_err_t rt_mutex_release(rt_mutex_t mutex);//互斥量对象的句柄
六、互斥量应用示例(时间问题此部分截取RTT源码示例)
有两个线程:线程 1 和线程 2,线程 1 对 2 个 number 分别进行加 1 操作;线程 2 也对 2 个 number 分别进行加 1 操作,使用互斥量保证线程改变 2 个 number 值的操作不被打断。如下代码所示:
#include <rtthread.h>
#define THREAD_PRIORITY 8
#define THREAD_TIMESLICE 5
/* 指向互斥量的指针 */
static rt_mutex_t dynamic_mutex = RT_NULL;
static rt_uint8_t number1,number2 = 0;
ALIGN(RT_ALIGN_SIZE)
static char thread1_stack[1024];
static struct rt_thread thread1;
static void rt_thread_entry1(void *parameter)
{
while(1)
{
/* 线程 1 获取到互斥量后,先后对 number1、number2 进行加 1 操作,然后释放互斥量 */
rt_mutex_take(dynamic_mutex, RT_WAITING_FOREVER);
number1++;
rt_thread_mdelay(10);
number2++;
rt_mutex_release(dynamic_mutex);
}
}
ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;
static void rt_thread_entry2(void *parameter)
{
while(1)
{
/* 线程 2 获取到互斥量后,检查 number1、number2 的值是否相同,相同则表示 mutex 起到了锁的作用 */
rt_mutex_take(dynamic_mutex, RT_WAITING_FOREVER);
if(number1 != number2)
{
rt_kprintf("not protect.number1 = %d, mumber2 = %d \n",number1 ,number2);
}
else
{
rt_kprintf("mutex protect ,number1 = mumber2 is %d\n",number1);
}
number1++;
number2++;
rt_mutex_release(dynamic_mutex);
if(number1>=50)
return;
}
}
/* 互斥量示例的初始化 */
int mutex_sample(void)
{
/* 创建一个动态互斥量 */
dynamic_mutex = rt_mutex_create("dmutex", RT_IPC_FLAG_PRIO);
if (dynamic_mutex == RT_NULL)
{
rt_kprintf("create dynamic mutex failed.\n");
return -1;
}
rt_thread_init(&thread1,
"thread1",
rt_thread_entry1,
RT_NULL,
&thread1_stack[0],
sizeof(thread1_stack),
THREAD_PRIORITY, THREAD_TIMESLICE);
rt_thread_startup(&thread1);
rt_thread_init(&thread2,
"thread2",
rt_thread_entry2,
RT_NULL,
&thread2_stack[0],
sizeof(thread2_stack),
THREAD_PRIORITY-1, THREAD_TIMESLICE);
rt_thread_startup(&thread2);
return 0;
}
/* 导出到 MSH 命令列表中 */
MSH_CMD_EXPORT(mutex_sample, mutex sample);