同步互斥与通信

目录

一、同步与互斥的概念

二、同步与互斥并不简单

三、各类方法的对比


一、同步与互斥的概念

一句话理解同步与互斥:我等你用完厕所,我再用厕所。

什么叫同步?就是:哎哎哎,我正在用厕所,你等会。

什么叫互斥?就是:哎哎哎,我正在用厕所,你不能进来。

同步与互斥经常放在一起讲,是因为它们之的关系很大,"互斥"操作可以使用"同步"来 实现。我"等"你用完厕所,我再用厕所。这不就是用"同步"来实现"互斥"吗?

再举一个例子。在团队活动里,同事A先写完报表,经理B才能拿去向领导汇报。

经理B必须等同事A完成报表,AB之间有依赖,B必须放慢脚步,被称为同步。在团队活动中,同 事A已经使用会议室了,经理B也想使用,即使经理B是领导,他也得等着,这就叫互斥。经 理B跟同事A说:你用完会议室就提醒我。这就是使用"同步"来实现"互斥"。

cs 复制代码
01 void 抢厕所(void)
02 {
03     if (有人在用) 我眯一会;
04     用厕所;
05     喂,醒醒,有人要用厕所吗;
06 }

假设有A、B两人早起抢厕所,A先行一步占用了;B慢了一步,于是就眯一会;当A用完后叫醒B,B也就愉快地上厕所了。

在这个过程中,A、B是互斥地访问"厕所","厕所"被称之为临界资源。我们使用了"休眠-唤醒"的同步机制实现了"临界资源"的"互斥访问"。

同一时间只能有一个人使用的资源,被称为临界资源。比如任务A、B都要使用串口来打印,串口就是临界资源。如果A、B同时使用串口,那么打印出来的信息就是A、B混杂, 无法分辨。所以使用串口时,应该是这样:A用完,B再用;B用完,A再用。

二、同步与互斥并不简单

在裸机程序里,可以使用一个全局变量或静态变量实现互斥操作,比如要互斥地使用 LCD,可以使用如下代码:

cs 复制代码
01 int LCD_PrintString(int x, int y, char *str)
02 {
03     static int bCanUse = 1;
04     if (bCanUse)
05     {
06         bCanUse = 0;
07         /* 使用LCD */
08         bCanUse = 1;
09         return 0;
10     }
11     return -1;
12 }

但是在 RTOS 里,使用上述代码实现互斥操作时,大概率是没问题的,但是无法确保万无一失。

假设如下场景:有两个任务 A、B 都想调用 LCD_PrintString,任务 A 执行到第 4 行代码时发现 bCanUse 为 1,可以进入 if 语句块,它还没执行第 6 句指令就被切换出去了 ;然后任务 B 也调用 LCD_PrintString,任务 B 执行到第 4 行代码时也发现 bCanUse 为 1,也可以进入 if 语句块使用 LCD。在这种情况下,使用静态变量并不能实现互斥操作

上述例子中,是因为第 4、第 6 两条指令被打断了,那么如下改进:在函数入口处先然让 bCanUse 减一。这能否实现万无一失的互斥操作呢?

cs 复制代码
01 int LCD_PrintString(int x, int y, char *str)
02 {
03     static int bCanUse = 1;
04     bCanUse--;
05     if (bCanUse == 0)
06     {
07         /* 使用LCD */
08         bCanUse++;
09         return 0;
10     }
11     else
12     {
13         bCanUse++;
14         return -1;
15     }
16 }

把第 4 行的代码使用汇编指令表示如下:

04.1 LDR R0, [bCanUse] // 读取bCanUse的值,存入寄存器R0

04.2 DEC R0, #1 // 把R0的值减一

04.3 STR R0, [bCanUse] // 把R0写入变量bCanUse

假设如下场景:有两个任务 A、B 都想调用 LCD_PrintString,任务 A 执行到第 04.1 行代码时读到的 bCanUse 为 1,存入寄存器 R0 就被切换出去了;然后任务 B 也调用LCD_PrintString,任务 B 执行到第 4 行时发现 bCanUse 为 1 并把它减为 0,执行到第 5 行代码时发现条件成立可以进入 if 语句块使用 LCD,然后任务 B 也被切换出去了;现在任务 A 继续运行第 04.2 行代码时 R0 为 1,运行到第 04.3 行代码时把 bCanUse 设置为 0,后续也能成功进入 if 的语句块。在这种情况下,任务 A、B 都能使用 LCD。

上述方法不能保证万无一失的原因在于:在判断过程中,被打断了。如果能保证这个过程不被打断,就可以了:通过关闭中断来实现。

示例 1 的代码改进如下:在第 5~7 行前关闭中断。

cs 复制代码
01 int LCD_PrintString(int x, int y, char *str)
02 {
03     static int bCanUse = 1;
04     disable_irq();        //关闭中断
05     if (bCanUse)
06     {
07         bCanUse = 0;
08         enable_irq();    //开启中断
09         /* 使用LCD */
10         bCanUse = 1;
11         return 0;
12     }
13     enable_irq();        //开启中断
14     return -1;
15 }

示例 2 的代码改进如下:在第 5 行前关闭中断。

cs 复制代码
01 int LCD_PrintString(int x, int y, char *str)
02 {
03     static int bCanUse = 1;
04     disable_irq();
05     bCanUse--;
06     enable_irq();
07     if (bCanUse == 0)
08     {
09         /* 使用LCD */
10         bCanUse++;
11         return 0;
12     }
13     else
14     {
15         disable_irq();
16         bCanUse++;
17         enable_irq();
18         return -1;
19     }
20 }

关闭中断的方法并不是万无一失的:假设现在有任务A和任务B在执行以下函数,在A打印后过了1ms,B被调度,但B只是进行判断,但会一直失败,过了1msA被调度继续打印,打印完后过了1ms轮到B,B也是继续判断然后一直失败,这样子导致的结果是B占用了cpu资源,与同步例子类似,那么此时解决方法是:将B执行时若A已经在打印了,将B设置为阻塞状态,于是A继续打印,等到A打印完毕,将B唤醒,B才能正常打印。

cs 复制代码
void CalTask(void *params)//计时函数
{
	uint32_t i = 0;
	
	time = system_get_ns();//程序执行到此的时间
	for(i=0;i<10000000;i++)
	{
		sum += i;
	}
	Cal_end = 1;//计算标志位置1
	time = system_get_ns() - time;//先计算程序到此的时间 再减去之前的时间 得到for循环中的时间
	vTaskDelete(NULL);
}

void LcdPrintTask(void *params)
{
	int len;
	
	while(1)
	{
		/* 打印信息 */
		LCD_PrintString(0,0,"waitting");
		
		vTaskDelay(2000);//让此任务进入阻塞状态 等上面任务执行完毕在执行这里 让下面的while死循环不会占用cpu资源 不调用此函数则不会进入阻塞 会一直死等 为同步例子
		
		while(Cal_end == 0);
		//这里是在等待上面的计数完毕 若不使用上一行的vTaskDelay函数 则会在这里死等 占用cpu资源 因为两个任务是交叉执行 若没有上一行的vTaskDelay函数 烧录完发现时间为2s左右 由于两个任务叫交叉执行 若将B任务删除只执行A任务 则时间差不多为一半即1ms 那么此时就可以调用vTaskDelay函数让此任务进入阻塞状态 让task1先执行 执行完毕计时标志位置1 同时vTaskDelay函数计时完毕后进入此时的死循环 此时计时标志位置1直接跳出循环 不会让程序卡在这里占用cpu资源 就可以准确计算出程序执行时间
			
		if(flag)
		{
			flag = 0;
			
			LCD_ClearLine(0,0);
			len = LCD_PrintString(0,0,"Sum:");//这里的返回值是打印任务的长度
			len += LCD_PrintHex(len,0,sum,1);//将打印名字处后打印":"的长度一起加上 得到的长度是打印完任务名字和":"后的长度
			
			LCD_ClearLine(0,2);
            len = LCD_PrintString(0,2,"Time(ms):");
			len += LCD_PrintSignedVal(len,2,time/1000000);
			
			flag = 1;
		}
		vTaskDelete(NULL);
	}
}

/* FreeRTOS.c中创建任务 */
xTaskCreate(CalTask,"taskA",128,NULL,osPriorityNormal,NULL);
xTaskCreate(LcdPrintTask,"taskB",128,&Task2,osPriorityNormal,NULL);

同步例子(任务B没有阻塞)

由结果可知,两个任务程序是交叉进行的,在RTOS中,任务级相同的任务每隔1s交叉运行,所以任务A执行1s(开始计时)后切到任务B,而A没执行完毕 flag 始终为 0,那么任务B就会死等(任务A没执行完毕,仍在计时) flag 为1,当B执行完A继续执行(继续计时)后 flag 为1后进入任务B才能成功计时完毕。可见时间大约为2s,若只有程序A执行,那么时间大约为1s左右。

互斥例子(任务B阻塞)

任务A不断计时,任务A执行完1s后任务B执行,而任务B中调用了阻塞函数,所以继续轮到任务A执行,任务A执行完毕跳转到任务B中B成功打印计时值。(这里的计数值主要来源于任务A,所以若任务A执行完毕,会停止计时,所以B中的阻塞延时(只是2s后延时)对于计时结果无影响,为了能够更好准确的得到计时值)

三、各类方法的对比

能实现同步、互斥的内核方法有:任务通知(task notification)、队列(queue)、事件组 (event group)、信号量(semaphoe)、互斥量(mutex)。

它们都有类似的操作方法:获取/释放、阻塞/唤醒、超时。比如:

  • 任务 A 获取资源,用完后任务 A 释放资源
  • 任务 A 获取不到资源则阻塞,任务 B 释放资源并把任务 A 唤醒
  • 任务 A 获取不到资源则阻塞,并定个闹钟;A 要么超时返回,要么在这段时间内因为任务 B 释放资源而被唤醒。

通过对比的方法来区分:

  • 能否传信息?还是只能传递状态?
  • 为众生(所有任务都可以使用)?只为你(只能指定任务使用)?
  • 我生产,你们消费?
  • 我上锁,只能由我开锁

|---------------|--------------|--------------|-----------------------------------------|-----------------------------------------------|
| 内核对 | 生产 | 消费 | 数据/状态 | 说明 |
| 队列 | ALL | ALL | 数据:若干个数据 谁都可以往队列里扔数据, 谁都可以从队列里读数据 | 用来传递数据, 发送者、接收者无限制, 一个数据只能唤醒一 个接收者 |
| 事件组 | ALL | ALL | 多个位:或、与 谁都可以设置(生产)多个 位, 谁都可以等待某个位、若 干个位 | 用来传递事件, 可以是 N 个事件, 发送者、接受者无限制, 可以唤醒多个接收 者:像广播 |
| 信号量 | ALL | ALL | 数量:0~n 谁都可以增加一个数量, 谁都可消耗一个数量 | 用来维持资源的个数, 生产者、消费者无限 制, 1 个资源只能唤醒 1 个接收者 |
| 任务通知 | ALL | 只有我 | 数据、状态都可以传输, 使用任务通知时, 必须指定接受者 | N 对 1 的关系: 发送者无限制, 接收者只能是这个任 务 |
| 互斥量 | A 上锁 | 只能 A 开锁 | 位:0、1 我上锁:1 变为 0, 只能由我开锁:0 变为 1 | 就像一个空厕所, 谁使用谁上锁, 也只能由他开锁 |

使用图形对比如下:

  • 队列:
    • 里面可以放任意数据,可以放多个数据
    • 任务、ISR 都可以放入数据;任务、ISR 都可以从中读出数据
  • 事件组:
    • 一个事件用一 bit 表示,1 表示事件发生了,0 表示事件没发生
    • 可以用来表示事件、事件的组合发生了,不能传递数据
    • 有广播效果:事件或事件的组合发生了,等待它的多个任务都会被唤醒
  • 信号量:
    • 核心是"计数值"
    • 任务、ISR 释放信号量时让计数值加 1
    • 任务、ISR 获得信号量时,让计数值减 1
  • 任务通知:
    • 核心是任务的 TCB 里的数值
    • 会被覆盖
    • 发通知给谁?必须指定接收任务
    • 只能由接收任务本身获取该通知
  • 互斥量:
    • 数值只有 0 或 1
    • 谁获得互斥量,就必须由谁释放同一个互斥量
相关推荐
7yewh8 小时前
Linux驱动开发 IIC I2C驱动 编写APP访问EEPROM AT24C02
linux·arm开发·驱动开发·嵌入式硬件·嵌入式
上海易硅智能科技局有限公司8 小时前
AG32 MCU 的电机控制方案
单片机·嵌入式硬件
程序员JerrySUN8 小时前
Yocto 项目 - 共享状态缓存 (Shared State Cache) 机制
linux·嵌入式硬件·物联网·缓存·系统架构
嵌入式小强工作室10 小时前
stm32能跑人工智能么
人工智能·stm32·嵌入式硬件
MikelSun10 小时前
电压控制环与电流控制环
单片机·嵌入式硬件·物联网
陌夏微秋11 小时前
STM32单片机芯片与内部45 UART 不定长度接收 标志位结束 定时器超时 串口空闲中断
stm32·单片机·嵌入式硬件·信息与通信·智能硬件
挥剑决浮云 -12 小时前
STM32学习之 按键/光敏电阻 控制 LED/蜂鸣器
c语言·经验分享·stm32·单片机·嵌入式硬件·学习
Whappy00113 小时前
第13部分 1.STM32之PWR电源控制-----4个实验
stm32·单片机·嵌入式硬件
冲,干,闯14 小时前
单片机里不想阻塞系统的延时
单片机·嵌入式硬件
小菜鸟学代码··14 小时前
STM32中断详解
stm32·单片机·嵌入式硬件