同步互斥
4.1 同步互斥概念¶
对于资源C,使用者A和B。
同步 :当A正在使用C时,B必须等待A使用完,其中A、B有依赖关系,即B依赖A产生的数据完成自己的任务。
互斥 :当A正在使用C时,B不能使用C,强调的时资源C同一时间只能有一个任务使用。
4.2 有缺陷的同步示例¶
在创建FreeRTOS项目时,由于RTOS时钟使用的是SysTick
,我选用的系统时钟为TIM6
。
借鉴百问网
时钟驱动程序,我们以TIM6
为时基,创建一个获取系统时间的驱动:
timer.h
#ifndef ROBOT_TIMER_H
#define ROBOT_TIMER_H
#include "stm32f1xx_hal.h"
uint64_t system_get_ns(void);
#endif //ROBOT_TIMER_H
timer.c
#include "timer.h"
/**********************************************************************
* 函数名称: system_get_ns
* 功能描述: 获得系统时间(单位ns)
* 输入参数: 无
* 输出参数: 无
* 返 回 值: 系统时间(单位ns)
* 修改日期 版本号 修改人 修改内容
* -----------------------------------------------
* 2023/08/03 V1.0 韦东山 创建
***********************************************************************/
uint64_t system_get_ns(void)
{
//extern uint32_t HAL_GetTick(void);
extern TIM_HandleTypeDef htim6;
TIM_HandleTypeDef *hHalTim = &htim6;
uint64_t ns = HAL_GetTick();
uint64_t cnt;
uint64_t reload;
cnt = __HAL_TIM_GET_COUNTER(hHalTim);
reload = __HAL_TIM_GET_AUTORELOAD(hHalTim);
ns *= 1000000;
ns += cnt * 1000000 / reload;
return ns;
}
任务函数:
/* 同步示例 */
void Task_A(void *argument)
{
uint32_t i;
OLED_ShowStr(0, 0, "Waiting", 16);
g_time = system_get_ns();
for (i = 0 ; i < 10000000 ; i++ )
{
g_cnt += i;
}
g_time = system_get_ns() - g_time;
g_count_end = 1;
vTaskDelete(NULL);
}
void Task_B(void *argument)
{
OLED_ShowStr(0, 0, "Waiting", 16);
// vTaskDelay(3000);
while (g_count_end ==0);
OLED_Clear();
OLED_ShowNum(0, 0, g_time/1000000, 11, 16);
OLED_ShowNum(0, 2, g_cnt, 11, 16);
vTaskDelete(NULL);
}
/* USER CODE END Application */
其中全局变量定义:
// 同步示例全局变量
static volatile uint8_t g_count_end = 0;
static uint32_t g_cnt=0;
static uint64_t g_time=0;
/* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN FunctionPrototypes */
void Task_B(void *argument);
void Task_A(void *argument);
/* USER CODE END FunctionPrototypes */
/* USER CODE BEGIN RTOS_THREADS */
/* add threads, ... */
xTaskCreate(Task_A, "TaskA", 128, NULL, osPriorityNormal, NULL);
xTaskCreate(Task_B, "TaskB", 128, NULL, osPriorityNormal, NULL);
/* USER CODE END RTOS_THREADS */
最后的输出结果:
然Task_B
中等待为死循环,不进入阻塞态,因此会频繁被切换到Task_B
却只是做延时任务,这便是我们同步示例的缺陷。在死循环前加上vTaskDelay(3000);
就会节省近一半时间:
void Task_B(void *argument)
{
OLED_ShowStr(0, 0, "Waiting", 16);
vTaskDelay(6000);
while (g_count_end ==0);
OLED_Clear();
OLED_ShowNum(0, 0, g_time/1000000, 11, 16);
OLED_ShowNum(0, 2, g_cnt, 11, 16);
vTaskDelete(NULL);
}
输出:
4.3 有缺陷的互斥示例¶
下面是一个用全局变量g_status
实现互斥的示例,但前面我们提到它是有缺陷的❓:
假设如下场景:有两个任务A、B都想调用OLED_Print_Task
,任务A执行到第4行代码时发现g_status
为1,可以进入if
语句块,它还没执行第6句指令就被切换出去了;然后任务B也调用OLED_Print_Task
,任务B执行到第4行代码时也发现bCanUse
为1,也可以进入if语句块使用OLED。在这种情况下,使用全局变量并不能实现互斥操作。
void OLED_Print_Task(void *argument) {
Params *p = argument;
uint32_t cnt = 0;
while (1) {
if (g_status) {
g_status = 0;
OLED_ShowStr(p->x, p->y, p->str, 16);
OLED_ShowNum(p->x + 6 * 8, p->y, cnt, 3, 16);
cnt++;
g_status = 1;
}
vTaskDelay(500);
}
}
此方法不能保证一定实现互斥,原因在于:在判断过程中,被打断了。如果在确保在判断时不被中断打断,就可以解决这一问题,因此优化代码如下:
void OLED_Print_Task(void *argument) {
Params *p = argument;
uint32_t cnt = 0;
while (1) {
disable_irq();
if (g_status) {
g_status = 0;
OLED_ShowStr(p->x, p->y, p->str, 16);
OLED_ShowNum(p->x + 6 * 8, p->y, cnt, 3, 16);
cnt++;
g_status = 1;
}
enable_irq();
vTaskDelay(500);
}
}
然而,使用关闭中断的方法保护临界资源会出现浪费CPU资源情况:
假设,两个任务调用OLED_Print_Task
,而每次都是A成功打印,B一直判断失败,此时B一直判断失败的时间内就会造成CPU浪费。
我们期望的结果时,B判断失败后进入阻塞态,等A执行完OLED_Print_Task
,唤醒B。
4.4 FreeRTOS解决方法¶
图片来源:第10章 同步互斥与通信 | 百问网
能实现同步、互斥的内核方法有: 任务通知(Task Notification) 、 队列(Queue) 、 事件组(Event Group) 、 信号量(Semaphore) 和 互斥量(Mutex)。