本文讲RT-Thread的线程间同步之信号量,包括为什么要进行线程间同步、信号量创建与删除、信号量获取与释放以及基于STM32的二值信号量示例和计算型信号量示例,采用RTT&正点原子联合出品潘多拉开发板进行实验。
1、什么是线程间同步?
同步是指按预定的先后次序进行运行,线程同步是指多个线程通过特定的机制来控制线程之间的执行顺序,也可以说是在线程之间通过同步建立起执行顺序的关系,如果没有同步,那线程之间将是无序的。
2、为什么要进行线程间同步?
例如一项工作中的两个线程:一个线程从传感器中接收数据并且将数据写到共享内存中,同时另一个线程周期性的从共享内存中读取数据并发送去显示,下图描述了两个线程间的数据传递:
如果对共享内存的访问不是排他性的,那么各个线程间可能同时访问它,这将引起数据一致性的问题。例如,在显示线程试图显示数据之前,接收线程还未完成数据的写入,那么显示将包含不同时间采样的数据,造成显示数据的错乱。将传感器数据写入到共享内存块的接收线程 #1 和将传感器数据从共享内存块中读出的线程 #2 都会访问同一块内存。为了防止出现数据的差错,两个线程访问的动作必须是互斥进行的,应该是在一个线程对共享内存块操作完成后,才允许另一个线程去操作,这样,接收线程 #1 与显示线程 #2 才能正常配合,使此项工作正确地执行。
信号量是一种轻型的用于解决线程间同步问题的内核对象,线程可以获取或释放它,从而达到同步或互斥的目的。通常一个信号量的计数值用于对应有效的资源数,表示剩下的可被占用的互斥资源数。其值的含义分两种情况:(1) 0,表示没有积累下来的 release 释放信号量操作,且有可能有在此信号量上阻塞的线程。(2)正值,表示有一个或多个 release 释放信号量操作。
1、 互斥为目的的信号量:用作互斥时,信号量创建后可用信号量个数应该是满的,线程在需要使用临界资源时,先获取信号量,使其变空,这样其他线程需要使用临界资源时就会因为无法获取信号量而进入阻塞,从而保证了临界资源的安全。
2、同步为目的的信号量:用作同步时,信号量在创建后被置为空,线程 1 取信号量而阻塞,线程 2 在某种条件发生后,释放信号量,于是线程 1 得以进入就绪态,如果线程 1 的优先级是最高的,那么就会立即切换线程,从而达到了两个线程间的同步。同样的,在中断服务函数中释放信号量,也能达到线程与中断间的同步。
3、创建动态信号量函数:当创建一个信号量时,内核首先创建一个信号量控制块,然后对该控制块进行基本的初始化工作。信号量标志参数flag决定了当信号量不可用时,多个线程等待的排队方式。当选择 RT_IPC_FLAG_FIFO(先进先出)方式时,那么等待线程队列将按照先进先出的方式排队,先进入的线程将先获得等待的信号量;当选择RT_IPC_FLAG_PRIO(优先级等待)方式时,等待线程队列将按照优先级进行排队,优先级高的等待线程将先获得等待的信号量。
rt_sem_t rt_sem_create(const char *name, rt_uint32_t value, rt_uint8_t flag);
(1)入口参数:
name:信号量名称。
value:信号量初始值。
flag:信号量标志,它可以取如下数值:RT_IPC_FLAG_FIFO 或RT_IPC_FLAG_PRIO。
(2)返回值
RT_NULL:创建失败。
信号量的控制块指针:创建成功。
4、删除动态信号量函数:系统不再使用动态信号量时,可通过删除动态信号量以释放系统资源,如果系统删除该信号量时,有线程正在等待该信号量,那么删除操作会先唤醒等待在该信号量上的线程(等待线程的返回值是- RT_ERROR),然后再释放信号量的内存资源。
rt_err_t rt_sem_delete(rt_sem_t sem);
(1)入口参数:
sem:创建的信号量对象。
(2)返回值:
RT_EOK:删除成功。
5、创建静态信号量函数:创建静态信号量也就是《RT-Thread编程指南》所讲的初始化信号量。对于静态信号量对象,它的内存空间在编译时期就被编译器分配出来,放在读写数据段或未初始化数据段上,此时使用信号量就不再需要使用 rt_sem_create 接口来创建它,而只需在使用前对它进行初始化即可。
rt_err_t rt_sem_init(rt_sem_t sem,
const char *name,
rt_uint32_t value,
rt_uint8_t flag);
(1)入口参数:
sem:信号量对象的句柄。
name:信号量名称。
value:信号量初始值。
flag:信号量标志,它可以取如下数值:RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO。
(2)返回值:
RT_EOK:初始化成功。
6、删除静态信号量函数:删除静态信号量也就是《RT-Thread编程指南》所讲的脱离信号量,就是让信号量对象从内核对象管理器中脱离。当不再需要静态信号量时,可删除静态信号量,内核先唤醒所有挂在该信号量等待队列上的线程,然后将该信号量从内核对象管理器中脱离,原来挂起在信号量上的等待线程将获得-RT_ERROR 的返回值。
rt_err_t rt_sem_detach(rt_sem_t sem);
(1)入口参数:
sem:信号量对象的句柄。
(2)返回值:
RT_EOK:脱离成功。
7、获取信号量函数:线程通过获取信号量来获得信号量资源实例,当信号量值大于零时,线程将获得信号量,并且相应的
信号量值会减 1。如果信号量的值等于零,那么说明当前信号量资源实例不可用,申请该信号量的线程将根据 time 参数的情况选择直接返回、或挂起等待一段时间、或永久等待,直到其他线程或中断释放该信号量。如果在参数 time 指定的时间内依然得不到信号量,线程将超时返回,返回值是-RT_ETIMEOUT。
rt_err_t rt_sem_take (rt_sem_t sem, rt_int32_t time);
(1)入口参数
sem:信号量对象的句柄。
time:指定的等待时间,单位是操作系统时钟节拍(OS Tick)。
(2)返回值
RT_EOK:成功获得信号量。
RT_ETIMEOUT:超时依然未获得信号量。
RT_ERROR:其他错误。
8、无等待获取信号量:当用户不想在申请的信号量上挂起线程进行等待时,可以使用无等待方式获取信号量,它的作用是和rt_sem_take(sem, 0) 一样的,即当线程申请的信号量资源实例不可用的时候,它不会等待在该信号量上,而是直接返回RT_ETIMEOUT。
rt_err_t rt_sem_trytake(rt_sem_t sem);
(1)入口参数:
sem:信号量对象的句柄。
(2)返回值:
RT_EOK:成功获得信号量。
RT_ETIMEOUT:获取失败。
9、释放信号量函数:释放信号量可以唤醒挂起在该信号量上的线程,当信号量的值等于零时,并且有线程等待这个信号量时,释放信号量将唤醒等待在该信号量线程队列中的第一个线程,由它获取信号量,同时将把信号量的值加 1。
rt_err_t rt_sem_release(rt_sem_t sem);
(1)入口参数:
sem:信号量对象的句柄。
(2)返回值:
RT_EOK:成功释放信号量。
1、二值信号量
因为信号量资源被获取了,信号量值就是 0,信号量资源被释放,信号量值就是 1,把这种只有 0和 1 两种情况的信号量称之为二值信号量。在嵌入式操作系统中二值信号量是线程间、线程与中断间同步的重要手段。
2、二值信号量的运作机制
创建二值信号量,为创建的信号量对象分配内存,并把可用信号量初始化为用户自定义的个数, 二值信号量的最大可用信号量个数为 1。信号量获取,从创建的信号量资源中获取一个信号量,获取成功返回正确。否则线程会等待其它线程释放该信号量,超时时间由用户设定。当线程获取信号量失败时,线程将进入阻塞态,系统将线程挂到该信号量的阻塞列表中。
(1)在二值信号量无效的时候,假如此时有线程获取该信号量的话,那么线程将进入阻塞状态,如下图所示。
(2)假如某个时间中断/线程释放了信号量,那么由于获取无效信号量而进入阻塞态的线程将获得信号量并且恢复为就绪态,如下图所示。
(3)二值信号量运作机制如下图所示。
3、计数型信号量
计数型信号量与二值信号量其实都是差不多的,一样用于资源保护,不过计数信号量则允许多个线程获取信号量访问共享资源,但会限制线程的最大数目。访问的线程数达到信号量可支持的最大数目时,会阻塞其他试图获取该信号量的线程,直到有线程释放了信号量。
4、计数型信号量的运作机制
虽然计数信号量允许多个线程访问同一个资源,但是也有限定,比如某个资源限定只能有 3 个线程访问,那么第 4 个线程访问的时候,会因为获取不到信号量而进入阻塞,等到有线程(比如线程 1)释放掉该资源的时候,第 4个线程才能获取到信号量从而进行资源的访问。
前面讲了很多关于信号量的概念知识,光说不练都是假把式,那么接下来我们就使用STM32L475VET6,RTT&正点原子联合出品的潘多拉开发板来进行实际操作。首先进行二值信号量的实验:创建一个信号量,两个线程,信号量值初始化为1,一个线程用于修改公共资源(定义一个全局变量作为公共资源)值,另一个线程用于获取公共资源值并通过FinSH打印处理,通过二值信号量从而实现只有在公共资源先被修改更新之后才能被获取。
1、实现代码
#include "main.h"
#include "board.h"
#include "rtthread.h"
#include "data_typedef.h"
#include "led.h"
int main(void)
{
binary_semaphore_test();
return 0;
}
/* 定义线程控制块 */
static rt_thread_t receive_data_thread = RT_NULL;
static rt_thread_t send_data_thread = RT_NULL;
/* 定义二值信号量控制块 */
static rt_sem_t binary_sem = RT_NULL;
/* 公共资源 */
u8 g_value = 1;
/**************************************************************
函数名称 : send_data_thread_entry
函数功能 : 修改公共资源线程入口函数
输入参数 : parameter:入口参数
返回值 : 无
备注 : 无
**************************************************************/
void send_data_thread_entry(void *parameter)
{
while(1)
{
/* 移植等,直到获取到信号量 */
rt_sem_take(binary_sem, RT_WAITING_FOREVER); /* 等待时间:一直等 */
if(g_value < 10)
{
g_value++;
}
else
{
g_value = 1;
}
rt_sem_release(binary_sem); /* 释放二值信号量 */
rt_thread_mdelay(1);
rt_thread_yield(); /* 放弃剩余时间片,进行一次线程切换 */
}
}
/**************************************************************
函数名称 : receive_data_thread_entry
函数功能 : 获取公共资源线程入口函数
输入参数 : parameter:入口参数
返回值 : 无
备注 : 无
**************************************************************/
void receive_data_thread_entry(void *parameter)
{
while(1)
{
/* 移植等,直到获取到信号量 */
rt_sem_take(binary_sem, RT_WAITING_FOREVER); /* 等待时间:一直等 */
rt_kprintf("g_value:%d\r\n", g_value);
LED_R(0);
rt_thread_mdelay(2000);
LED_R(1);
rt_thread_mdelay(2000);
rt_sem_release(binary_sem);/* 释放二值信号量 */
}
}
/**************************************************************
函数名称 : binary_semaphore_test
函数功能 : 创建二值信号量、创建数据修改线程、接收数据线程
输入参数 : parameter:入口参数
返回值 : 无
备注 : 无
**************************************************************/
void binary_semaphore_test(void)
{
binary_sem = rt_sem_create("binary_sem", /* 信号量名字 */
1, /* 信号量初始值 */
RT_IPC_FLAG_FIFO/* 先进先出 */
);
if(RT_NULL != binary_sem)/* 创建信号量成功 */
{
rt_kprintf("The semaphore was successfully created\r\n");
}
else/* 创建信号量失败 */
{
rt_kprintf("Failed to create semaphore\r\n");
return;
}
/* 创建接收数据线程 */
receive_data_thread = rt_thread_create("receive_data",
receive_data_thread_entry,/* 线程入口函数 */
RT_NULL, /* 入口参数 */
512, /* 栈大小 */
4, /* 优先级 */
50 /* 时间片 */
);
/* 创建线程成功,则启动线程 */
if(receive_data_thread != RT_NULL)
{
rt_thread_startup(receive_data_thread);
}
else
{
rt_kprintf("Failed to create receive_data_thread\r\n");
return;
}
/* 创建发送数据线程 */
send_data_thread = rt_thread_create("send_data",
send_data_thread_entry,/* 线程入口函数 */
RT_NULL, /* 入口参数 */
512, /* 栈大小 */
3, /* 优先级 */
50 /* 时间片 */
);
/* 创建线程成功,则启动线程 */
if(send_data_thread != RT_NULL)
{
rt_thread_startup(send_data_thread);
}
else
{
rt_kprintf("Failed to create send_data_thread\r\n");
return;
}
}
2、观察FinSH
开机,可看到每隔2000ms,会打印g_value会改变的值,输入list_sem可查看到当前挂起正在等待信号量的线程。
使用潘多拉开发板进行计算型信号量的实验,模拟停车场:创建一个信号量,两个线程,其中一个线程用于通过按下按键KEY0获取信号量值,类似获取停车场是否有车位;另外一个线程通过按下按键KEY1用于释放信号量,类似停车场多出车位。当按下按键KEY0后为RGB红灯亮,表示获取信号量失败(信号量值为0),没有停车位,当按下按键KEY0后RGB绿灯亮表示获取信号量成功(信号量值为正值),有停车位。按下按键KEY1后释放信号量来增加停车位。
1、实现代码
#include "main.h"
#include "board.h"
#include "rtthread.h"
#include "data_typedef.h"
#include "led.h"
#include "key.h"
#include "beep.h"
/* 定义线程控制块 */
static rt_thread_t get_parking_place_thread = RT_NULL;
static rt_thread_t release_parking_place_thread = RT_NULL;
/* 定义计算型信号量控制块 */
static rt_sem_t counting_sem = RT_NULL;
void get_parking_place_thread_entry(void *parameter)
{
rt_err_t res = RT_EOK;
while(1)
{
if(key_scan(0) == KEY0_PRES)
{
//res = rt_sem_take(counting_sem, 0);/* 不等待 */
res = rt_sem_trytake(counting_sem);/* 无等待获取 */
if(RT_EOK == res)/* 有车位,可停车 */
{
LED_R(1);
LED_G(0);
rt_kprintf("have parking place\r\n");
}
else/* 无车位 */
{
LED_R(0);
LED_G(1);
rt_kprintf("no parking place\r\n");
}
}
rt_thread_mdelay(1);
}
}
void release_parking_place_thread_entry(void *parameter)
{
rt_err_t res = RT_EOK;
while(1)
{
if(key_scan(0) == KEY1_PRES)
{
res = rt_sem_release(counting_sem);
if(RT_EOK == res)
{
rt_kprintf("release paring place\r\n");
BEEP(1);
rt_thread_mdelay(200);
BEEP(0);
rt_thread_mdelay(200);
}
else
{
rt_kprintf("can not release paring place\r\n");
}
}
rt_thread_mdelay(1);
}
}
void counting_semaphore_test(void)
{
counting_sem = rt_sem_create("counting_sem", /* 信号量名字 */
0, /* 信号量初始值为0 */
RT_IPC_FLAG_FIFO /* 先进先出 */
);
if(RT_NULL != counting_sem)/* 创建信号量成功 */
{
rt_kprintf("The counting semaphore was successfully created\r\n");
}
else/* 创建信号量失败 */
{
rt_kprintf("Failed to create counting semaphore\r\n");
return;
}
/* 创建接收数据线程 */
get_parking_place_thread = rt_thread_create("get_parking_place",
get_parking_place_thread_entry,/* 线程入口函数 */
RT_NULL,/* 入口参数 */
512, /* 栈大小 */
4, /* 优先级 */
50 /* 时间片 */
);
/* 创建线程成功,则启动线程 */
if(get_parking_place_thread != RT_NULL)
{
rt_thread_startup(get_parking_place_thread);
}
else
{
rt_kprintf("Failed to create get_parking_place_thread_entry\r\n");
return;
}
release_parking_place_thread=rt_thread_create("release_parking_place", release_parking_place_thread_entry,
RT_NULL,/* 入口参数 */
512, /* 栈大小 */
3, /* 优先级 */
50 /* 时间片 */
);
/* 创建线程成功,则启动线程 */
if(release_parking_place_thread != RT_NULL)
{
rt_thread_startup(release_parking_place_thread);
}
else
{
rt_kprintf("Failed to create release_parking_place_thread\r\n");
return;
}
}
2、观察FinSH
(1)开机,按下按键KEY0获取信号量,打印no parking place,同时RGB红灯亮,获取信号量失败,那是因为我们信号量初始值为0,list_sem,counting_sem的值为0:
(2)按下按键KEY1,打印release paring place,释放了信号量,多出一个车位,然后list_sem,counting_sem的值变为1:
(3)再次按下按键KEY1,打印release paring place,释放了信号量:然后list_sem,counting_sem的值变为2:
(4)这时,按下按键KEY0,打印have parking place,获取信号量成功,同时RGB绿灯亮,list_sem,counting_sem的值变为1:
1、使用信号量会导致的另一个潜在问题是线程优先级翻转问题。所谓优先级翻转,即当一个高优先级线程试图通过信号量机制访问共享资源时,如果该信号量已被一低优先级线程持有,而这个低优先级线程在运行过程中可能又被其它一些中等优先级的线程抢占,因此造成高优先级线程被许多具有较低优先级的线程阻塞,实时性难以得到保证。如下图所示:有优先级为 A、B 和 C 的三个线程,优先级 A> B > C。线程 A,B 处于挂起状态,等待某一事件触发,线程 C 正在运行,此时线程 C 开始使用某一共享资源 M。在使用过程中,线程 A 等待的事件到来,线程 A 转为就绪态,因为它比线程 C 优先级高,所以立即执行。但是当线程 A 要使用共享资源 M 时,由于其正在被线程 C 使用,因此线程 A 被挂起切换到线程 C 运行
2、如果此时线程 B 等待的事件到来,则线程 B 转为就绪态。由于线程 B 的优先级比线程 C 高,因此线程 B开始运行,直到其运行完毕,线程 C 才开始运行。只有当线程 C 释放共享资源 M 后,线程 A 才得以执行。在这种情况下,优先级发生了翻转:线程 B 先于线程 A 运行。这样便不能保证高优先级线程的响应时间。
1、[野火®]《RT-Thread 内核实现与应用开发实战—基于STM32》
2、《RT-THREAD 编程指南》