在两个或多个线程会操作到同一个临界区资源的时候,会导致竞态的产生,需要通过线程间同步来避免出现错误,同步的方法有很多种,常使用信号量、互斥量(互斥锁)、事件集等。
一、信号量
每个信号量都有一个信号量值和线程等待队列,信号量值表示有几个线程可以操作此资源,没有一个线程使用此资源,信号量值减1,当为0时,再申请此信号量就会挂起在此信号量的等待队列上。
1、信号量相关函数
rt_err_t rt_sem_init(rt_sem_t sem,
const char *name,
rt_uint32_t value,
rt_uint8_t flag);//静态创建信号量
rt_err_t rt_sem_detach(rt_sem_t sem);//信号量脱离,对于静态创建的信号量
rt_sem_t rt_sem_create(const char *name, rt_uint32_t value, rt_uint8_t flag);//动态创建信号量
rt_err_t rt_sem_delete(rt_sem_t sem);//信号量删除,对于动态创建的信号量
rt_err_t rt_sem_take(rt_sem_t sem, rt_int32_t time);//获取信号量,如果信号量值为0,由time决定等待时间
rt_err_t rt_sem_trytake(rt_sem_t sem);//获取信号量,如果信号量值为0,直接返回
rt_err_t rt_sem_release(rt_sem_t sem);//释放信号量,信号量值加1
rt_err_t rt_sem_control(rt_sem_t sem, int cmd, void *arg);//信号量控制函数,使用相关命令可以复位信号量
2、信号量的创建和删除
- 静态创建:
flag可选RT_IPC_FLAG_FIFO(按获取该信号量的先后顺序)和RT_IPC_FLAG_PRIO(按线程优先级排序)。
rt_err_t rt_sem_init(rt_sem_t sem,//信号量控制块结构体
const char *name,//信号量名称
rt_uint32_t value,//信号量值
rt_uint8_t flag);//挂起的线程排队顺序
删除时使用 rt_sem_detach(rt_sem_t sem)函数。
- 动态创建:
rt_sem_t rt_sem_create(const char *name, rt_uint32_t value, rt_uint8_t flag);//与静态创建的参数意义一样
删除时使用rt_sem_delete(rt_sem_t sem)函数。
3、信号量的获取和释放
- 获取:
time表示当信号量值为0时要等待多久,可选RT_WAITING_FOREVER(一直等待)和(不等待,直接返回),如果给的大于0的数,则等待time时间。
每获取成功一次,信号量值减1。
rt_err_t rt_sem_take(rt_sem_t sem, rt_int32_t time);
- 释放:
释放信号量,信号量值加1。
rt_err_t rt_sem_release(rt_sem_t sem);
二、互斥量
相当于特殊的信号量——二值信号量,只能由同一线程进行释放,使用的函数与信号量差不多。
- 相关函数:
rt_err_t rt_mutex_init(rt_mutex_t mutex, const char *name, rt_uint8_t flag);
rt_err_t rt_mutex_detach(rt_mutex_t mutex);
rt_mutex_t rt_mutex_create(const char *name, rt_uint8_t flag);
rt_err_t rt_mutex_delete(rt_mutex_t mutex);
rt_err_t rt_mutex_take(rt_mutex_t mutex, rt_int32_t time);
rt_err_t rt_mutex_release(rt_mutex_t mutex);
rt_err_t rt_mutex_control(rt_mutex_t mutex, int cmd, void *arg);
三、事件集
事件集可以实现一对多,多对多的线程间同步,事件集由一个32位的数据表示,每一位表示一个事件,所以一个事件集最多有32个事件,只能用作同步,不能进行线程间的通信。事件集的创建与信号量类似。
1、相关函数:
rt_err_t rt_event_init(rt_event_t event, const char *name, rt_uint8_t flag);//静态创建事件集
rt_err_t rt_event_detach(rt_event_t event);//静态事件集脱离
rt_event_t rt_event_create(const char *name, rt_uint8_t flag);//动态创建事件集
rt_err_t rt_event_delete(rt_event_t event);//删除事件集
rt_err_t rt_event_send(rt_event_t event, rt_uint32_t set);//向事件集发送事件,将对应位置1
rt_err_t rt_event_recv(rt_event_t event,
rt_uint32_t set,
rt_uint8_t opt,
rt_int32_t timeout,
rt_uint32_t *recved);//释放对应位的事件
rt_err_t rt_event_control(rt_event_t event, int cmd, void *arg);//事件控制函数,使用相应命令可以复位事件集
2、事件的发送和接收:
- 发送:
set是一个32位数据,将要使用的事件对应位置1。
rt_err_t rt_event_send(rt_event_t event, rt_uint32_t set);
- 接收:
opt用来选择是否由多个事件决定,可选RT_EVENT_FLAG_AND和RT_EVENT_FLAG_OR,这两个必须选择一个,还可以再选择RT_EVENT_FLAG_CLEAR(接收完后将对应位置0)。
rt_err_t rt_event_recv(rt_event_t event,//事件集控制块结构体
rt_uint32_t set,//检查事件对用位是否为1
rt_uint8_t opt,//选择是否由多个事件决定
rt_int32_t timeout,//对应位为0是,选择等待时间,与信号量类似
rt_uint32_t *recved);//用来接收事件的值,如果不需要使用到则给RT_NULL
四、遇到的问题
原本是想要按键单击开启led1线程,led1灯闪烁,按键双击关闭led1线程,但是官方库函数中没有真正可以关闭线程的函数,rt_thread_suspend函数又只能自身线程调用。
查了资料后决定使用信号量来控制线程的关闭,信号量初始化为0,在按键双击触发的时候,释放信号量,led1回调函数中就会成功获取到信号量,就会调用rt_thread_suspend函数,将led1线程关闭。
led1线程回调函数修改成:
/***********************************************
* @brief : LED1回调函数
* @param : parameter:需要传入的参数
* @return: void
* @date : 2023.8.11
* @author: L
************************************************/
void LedThread1(void *parameter)
{
while(1)
{
if(rt_sem_take(stop_sem, RT_WAITING_NO) == RT_EOK)
{
rt_thread_suspend(led1);//挂起当前线程
rt_schedule();//开启调度
}
else
{
GPIO_ResetBits(GPIOC, GPIO_Pin_13);//灯亮
rt_kprintf("led1 on\r\n");
rt_thread_mdelay(500);
GPIO_SetBits(GPIOC, GPIO_Pin_13);//灯灭
rt_kprintf("led1 off\r\n");
}
rt_thread_mdelay(500);
}
}
按键双击函数:
/***********************************************
* @brief : 按键双击任务函数
* @param : 选择是哪个按键
* @return: void
* @date : 2023.8.11
* @author: L
************************************************/
void DoubleTick(void *parameter)
{
switch(*(uint8_t*)parameter)
{
case 1://按键1
{
rt_sem_release(stop_sem);//释放信号量,停止led1线程
break;
}
default:break;
}
}