目录
- 同步与互斥通信
- 同步与互斥的概念
- 同步与互斥并不简单
- 缺陷
- 分析汇编指令
- 优化过程 - 关闭中断
- 时间轴分析
- 思考时刻
参考《FreeRTOS入门与工程实践(基于DshanMCU-103).pdf》
同步与互斥通信
同步与互斥的概念
一句话理解同步与互斥:我等你用完厕所,我再用厕所。
什么叫同步?就是:哎哎哎,我正在用厕所,你等会。 什么叫互斥?就是:哎哎哎,我正在用厕所,你不能进来。
同步与互斥经常放在一起讲,是因为它们之的关系很大,“互斥”操作可以使用“同步”来实现。我“等”你用完厕所,我再用厕所。这不就是用“同步”来实现“互斥”吗?
再举一个例子。在团队活动里,同事A先写完报表,经理B才能拿去向领导汇报。经理B必须等同事A完成报表,AB之间有依赖,B必须放慢脚步,被称为同步。在团队活动中,同事A已经使用会议室了,经理B也想使用,即使经理B是领导,他也得等着,这就叫互斥。经理B跟同事A说:你用完会议室就提醒我。这就是使用"同步"来实现"互斥"。
有时候看代码更容易理解,伪代码如下:
void 抢厕所(void)
{
if (有人在用) 我眯一会;
用厕所;
喂,醒醒,有人要用厕所吗;
}
假设有A、B两人早起抢厕所,A先行一步占用了;B慢了一步,于是就眯一会;当A用完后叫醒B,B也就愉快地上厕所了。
在这个过程中,A、B是互斥地访问“厕所”,“厕所”被称之为临界资源。我们使用了“休眠-唤醒”的同步机制实现了“临界资源”的“互斥访问”。
同一时间只能有一个人使用的资源,被称为临界资源。比如任务A、B都要使用串口来打印,串口就是临界资源。如果A、B同时使用串口,那么打印出来的信息就是A、B混杂,无法分辨。所以使用串口时,应该是这样:A用完,B再用;B用完,A再用。
同步与互斥并不简单
假设两个任务都是OLED显示的操作,底层都是IIC代码
假设任务A发送了起始信号,此时发生一次调度,任务B也发送了起始信号
这样IIC时序就被打乱了,IIC时序烂七八糟的
- 所以使用红框的代码,必须互斥的访问
假设A运行到108行,被切换出去了(全局变量还没有清零),运行任务B,任务B能正常运行,假设任务B运行到一半被切换到任务A,任务A才开始清零,A也可以使用LCD
如果切换任务发生在108行这一点上,那么A和B都可以同时使用LCD,但是这个概率非常非常低,基本不会在这里产生Tick中断
- 大部分时间,我们使用全局变量保护这个LCD是没有问题的,但是也有缺陷!~
- 程序运行很长时间,就有可能发送这样的缺陷!
缺陷
这个缺陷问题在于判断这个变量和设置这个变量被打断了!
在裸机程序里,可以使用一个全局变量或静态变量实现互斥操作,比如要互斥地使用LCD,可以使用如下代码:
int LCD_PrintString(int x, int y, char *str)
{
static int bCanUse = 1;
if (bCanUse)
{
bCanUse = 0;
/* 使用LCD */
bCanUse = 1;
return 0;
}
return -1;
}
但是在RTOS里,使用上述代码实现互斥操作时,大概率是没问题的,但是无法确保万无一失。
假设如下场景:有两个任务A、B都想调用LCD_PrintString,任务A执行到第4行代码时发现bCanUse为1,可以进入if语句块,它还没执行第6句指令就被切换出去了;然后任务B也调用LCD_PrintString,任务B执行到第4行代码时也发现bCanUse为1,也可以进入if语句块使用LCD。在这种情况下,使用静态变量并不能实现互斥操作。
上述例子中,是因为第4、第6两条指令被打断了,那么如下改进:在函数入口处先然让bCanUse减一。这能否实现万无一失的互斥操作呢?
int LCD_PrintString(int x, int y, char *str)
{
static int bCanUse = 1;
bCanUse--;
if (bCanUse == 0)
{
/* 使用LCD */
bCanUse++;
return 0;
}
else
{
bCanUse++;
return -1;
}
}
进入这个函数,bCanUse变量就减减,初值=1,减减后等于0
然后判断这个变量是否等于0,然后就执行下面的语句了
如果任务A能够执行完这个自减语句,才发生调度,执行任务B,在任务B里bCanUse先减减,减完就变成-1了,无法使用LCD
分析汇编指令
把bCanuse指令拆分一下,把第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行前关闭中断。
没有办法产生Tick中断,无法调度,所以就不会冲突了~
int LCD_PrintString(int x, int y, char *str)
{
static int bCanUse = 1;
disable_irq();
if (bCanUse)
{
bCanUse = 0;
enable_irq();
/* 使用LCD */
bCanUse = 1;
return 0;
}
enable_irq();
return -1;
}
示例2的代码改进如下:在第5行前关闭中断。
int LCD_PrintString(int x, int y, char *str)
{
static int bCanUse = 1;
disable_irq();
bCanUse--;
enable_irq();
if (bCanUse == 0)
{
/* 使用LCD */
bCanUse++;
return 0;
}
else
{
disable_irq();
bCanUse++;
enable_irq();
return -1;
}
}
时间轴分析
现在任务A和任务B都使用同一个函数,都想打印自己的信息
看时间轴,B在不断地判断,不断地失败,耗费了CPU资源
这种使用全局变量和关闭中断的方法,可以实现打印,但是太消耗CPU资源
那我们能不能实现A打印的过程中,B再来访问,B就阻塞,等待A执行完,用A唤醒B? 如下图所示:
思考时刻
- 后面用信号量和互斥量完成这些目标