自学内容网 自学内容网

同步互斥与通信

目录

一、同步与互斥的概念

二、同步与互斥并不简单

三、各类方法的对比


一、同步与互斥的概念

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

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

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

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

再举一个例子。在团队活动里,同事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
    • 谁获得互斥量,就必须由谁释放同一个互斥量


原文地址:https://blog.csdn.net/2301_78772787/article/details/140034474

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!