目录
一、同步与互斥的概念
二、同步与互斥并不简单
三、各类方法的对比
一、同步与互斥的概念
一句话理解同步与互斥:我等你用完厕所,我再用厕所。
什么叫同步?就是:哎哎哎,我正在用厕所,你等会。
什么叫互斥?就是:哎哎哎,我正在用厕所,你不能进来。
同步与互斥经常放在一起讲,是因为它们之的关系很大,“互斥”操作可以使用“同步”来 实现。我“等”你用完厕所,我再用厕所。这不就是用“同步”来实现“互斥”吗?
再举一个例子。在团队活动里,同事A先写完报表,经理B才能拿去向领导汇报。
经理B必须等同事A完成报表,AB之间有依赖,B必须放慢脚步,被称为同步。在团队活动中,同 事A已经使用会议室了,经理B也想使用,即使经理B是领导,他也得等着,这就叫互斥。经 理B跟同事A说:你用完会议室就提醒我。这就是使用"同步"来实现"互斥"。
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,可以使用如下代码:
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 减一。这能否实现万无一失的互斥操作呢?
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 行前关闭中断。
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 行前关闭中断。
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才能正常打印。
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
- 谁获得互斥量,就必须由谁释放同一个互斥量