FreeRTOS同步和互斥
目录
- 1.概念
- **1.1 同步(Synchronization)**
- 缺陷
- **1.2 互斥(Mutual Exclusion)**
- 缺陷
- **1.3 同步与互斥的关系**
- 2.各类方法对比
- 疑问
- 疑问1
1.概念
1.1 同步(Synchronization)
同步是指多个任务之间存在依赖关系,一个任务需要等待另一个任务完成某个操作后,才能继续执行。
现实生活例子:
- 在团队合作中,同事A先写完报表,经理B才能向领导汇报。经理B必须等同事A完成,这种任务之间的依赖关系就是同步。
- 在餐厅里,顾客点了餐,厨师必须先做好菜,然后服务员才能上菜给顾客。顾客的用餐取决于厨师的烹饪,这也是同步。
计算机中的同步
- 线程A先加载数据,线程B需要等A加载完成后再处理数据。
- 生产者-消费者模型中,消费者必须等待生产者生产数据后才能消费。
void 同事A_写报表(void) {printf("A: 正在写报表...\n");sleep(2); // 模拟报表书写时间printf("A: 报表写完了,通知经理B\n");报表完成 = 1; // 设置标志
}void 经理B_汇报(void) {while (报表完成 == 0) { // B必须等待A写完报表printf("B: 等待A完成报表...\n");sleep(1);}printf("B: 报表完成了,现在去向领导汇报\n");
}
经理B必须等同事A写完报表后,才能去汇报。
缺陷
当同步通过忙等待(busy waiting)的方式来实现时 :
- 但是同步有个缺陷,就比如上面代码这个例子来所,经理等待同事A写报表,这个等待并不是说经理一定就休眠了,不去不断抢夺CPU资源,经理要向领导汇报,那他肯定会不断去问好了没,同事A在写报表,不断被经理追问,这肯定就会导致同事A的效率变低。所以同步的缺陷就是正在执行的任务在另外一个等待执行任务的不间断尝试抢夺CPU资源的影响,导致速率变慢
- 为了避免忙等待带来的问题,许多同步机制采用了休眠/唤醒机制(如条件变量、信号量等)。当一个任务等待某个条件时,它会被挂起,不再占用 CPU,直到有其他任务完成操作后通知它“醒来”。这样可以大大降低 CPU 的无谓占用,提高系统整体效率。
来看看下面的代码:
static int sum = 0;
static volatile int flagCalcEnd = 0;void Task1Function(void * param)
{volatile int i = 0;while (1){for (i = 0; i < 10000000; i++)sum++;//printf("1");flagCalcEnd = 1;vTaskDelete(NULL);}
}void Task2Function(void * param)
{while (1){if (flagCalcEnd)printf("sum = %d\r\n", sum);}
}int main( void )
{TaskHandle_t xHandleTask1;#ifdef DEBUGdebug();
#endifprvSetupHardware();printf("Hello, world!\r\n");xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);//task2注释前后进行对比xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);/* Start the scheduler. */vTaskStartScheduler();/* Will only get here if there was not enough heap space to create theidle task. */return 0;
}
没注释xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);
前:
注释后:
通过时间对比还是可以明显看出,注释到后task1的运行速度快了一倍,这是因为没有Task2Function
去和task1抢夺资源,毕竟task2在这里使用的并不是休眠
1.2 互斥(Mutual Exclusion)
互斥是指多个任务在访问同一个共享资源时,必须互相排斥,即同一时间只能有一个任务使用该资源,其他任务必须等待。
现实生活例子:
- 上厕所问题:如果A先进入厕所,B必须等A出来后才能使用。厕所是临界资源,同一时间只能有一个人使用,避免资源冲突。
- 会议室使用:如果某个团队正在开会,其他人必须等待会议结束才能使用会议室。
计算机中的互斥
- 多个线程同时写入同一个文件,必须确保只有一个线程能写,否则数据可能会混乱。
- 多个任务同时访问串口打印数据时,必须互斥,否则打印输出会混杂。
mutex_t 厕所锁; // 定义一个互斥锁void 人A_上厕所(void) {mutex_lock(&厕所锁); // A 进入厕所,锁定资源printf("A: 我正在上厕所,B不能进来\n");sleep(2); // 模拟上厕所时间printf("A: 上完厕所,释放资源\n");mutex_unlock(&厕所锁); // A 释放资源
}void 人B_上厕所(void) {mutex_lock(&厕所锁); // B 进入厕所,锁定资源printf("B: 我正在上厕所,A不能进来\n");sleep(2);printf("B: 上完厕所,释放资源\n");mutex_unlock(&厕所锁); // B 释放资源
}
- mutex_lock() 确保同时只有一个人能进入厕所。
- mutex_unlock() 让下一个人可以使用厕所。
- 这样可以避免多个任务同时访问厕所导致的冲突(如两个人抢厕所)。
缺陷
当然上面的例子是通过锁来实现互斥的,如果使用一个全局变量来模拟实现互斥,也是会有缺陷的
static volatile int flagUARTused = 0;void TaskGenericFunction(void * param)
{while (1){if (!flagUARTused){flagUARTused = 1;printf("%s\r\n", (char *)param);flagUARTused = 0;vTaskDelay(1);}}
}//假设还有其它高优先级任务
//这个高优先级任务是有执行到taskDelay的,也就是休眠/*-----------------------------------------------------------*/int main( void )
{TaskHandle_t xHandleTask1;#ifdef DEBUGdebug();
#endifprvSetupHardware();printf("Hello, world!\r\n");//假设我还创建了一个高优先级任务xTaskCreate(TaskGenericFunction, "Task1", 100, "Task 1 is running", 1, NULL);xTaskCreate(TaskGenericFunction, "Task2", 100, "Task 2 is running", 1, NULL);/* Start the scheduler. */vTaskStartScheduler();/* Will only get here if there was not enough heap space to create theidle task. */return 0;
}
在这个代码中使用的是用一个全局变量flagUARTused来实现互斥,公共资源是TaskGenericFunction
打印函数
task1和task2两个任务,假设在执行task1的时候,如果它在运行到TaskGenericFunction
函数中的flagUARTused = 1;
时,高优先级任务休眠结束,抢占了资源,之后结束运行了,task1没抢到反而被task2抢到了,运行打印函数时if (!flagUARTused)
反而成立了,访问到了公共资源。
但是之前的task1,还没访问到,这就违背了互斥,我task1明明先用到了,我打印一半就被别人抢去了,这还能叫互斥?
1.3 同步与互斥的关系
同步和互斥经常一起使用,因为互斥可以通过同步来实现。
例如,在上厕所的场景中:
- 互斥:A、B不能同时用厕所。
- 同步:B必须等待A用完厕所,然后才能进去(A用完后通知B)。
mutex_t 厕所锁;
int 轮到谁 = 1; // 1代表A先上,2代表B上void 人A_上厕所(void) {while (轮到谁 != 1) { sleep(1); } // B正在用,A等待mutex_lock(&厕所锁);printf("A: 正在用厕所...\n");sleep(2);printf("A: 用完了,通知B\n");轮到谁 = 2; // 轮到B上厕所mutex_unlock(&厕所锁);
}void 人B_上厕所(void) {while (轮到谁 != 2) { sleep(1); } // A正在用,B等待mutex_lock(&厕所锁);printf("B: 正在用厕所...\n");sleep(2);printf("B: 用完了,通知A\n");轮到谁 = 1; // 轮到A上厕所mutex_unlock(&厕所锁);
}
- 互斥:使用
mutex_lock()
和mutex_unlock()
确保同时只有一个人能用厕所。 - 同步:使用
轮到谁
变量,让A、B按顺序使用厕所,保证流程正确。
概念 | 解释 | 现实例子 | 代码示例 |
---|---|---|---|
同步 | 任务间有依赖,必须按顺序执行 | 经理B必须等A写完报表才能汇报 | while(报表未完成) { 等待 } |
互斥 | 任务争抢资源,必须轮流使用 | 会议室一次只能开一个会 | mutex_lock(资源) |
- 同步:强调任务的先后顺序,任务A完成后,任务B才能开始。
- 互斥:强调资源的独占,同一时间只能一个任务访问资源。
2.各类方法对比
能实现同步、互斥的内核方法有:
- 任务通知(task notification)
- 队列(queue)
- 事件组(event group)
- 信号量(semaphoe)
- 互斥量(mutex)
对象类型 | 生产者/消费者关系 | 能否传递数据/状态 | 主要用途 |
---|---|---|---|
队列 (Queue) | 发送者、接收者均无限制,多对多 | 可以传递任意数据,多个数据项 | 用于传递数据,任务或ISR都可以入队和出队;一个数据通常只能唤醒一个接收者 |
事件组 (Event Group) | 发送者、接收者均无限制,多对多 | 传递的是事件状态,每个位表示一个事件(1:发生,0:未发生) | 用于传递事件或者事件组合,有类似广播的效果,可唤醒多个等待任务 |
信号量 (Semaphore) | 发送者、接收者均无限制,多对多 | 传递计数值(范围0~n),仅反映资源数量 | 主要用来管理资源个数,获取信号量代表占用资源,释放信号量代表资源释放;一个资源唤醒一个等待者 |
任务通知 (Task Notification) | 发送者无限制,接收者只能是指定任务(一对多转为N对1) | 可以传递简单的数据或状态,但后续通知会覆盖之前的通知 | 轻量级的同步方式,必须指定接收任务,适合在任务之间传递状态或简单信息 |
互斥量 (Mutex) | 只能由同一任务先“上锁”,后“释放” | 仅能传递0和1的状态,反映锁定或空闲状态 | 用于保护临界资源,同一时间只有一个任务能访问;获取锁的任务必须自己释放 |
小提示:
这些内核对象都支持类似的操作:获取/释放、阻塞/唤醒、以及超时机制。例如:
- 任务 A 获取资源(进入临界区),用完后释放资源。
- 如果 A 获取不到资源,则阻塞等待;当 B 释放资源时,可以唤醒 A。
- 若等待超时,则 A 会返回超时错误或采取其他处理措施。
队列:
- 里面可以放任意数据,可以放多个数据, 适合在任务或ISR之间传递复杂的数据结构。
- 任务、ISR都可以放入数据;任务、ISR都可以从中读出数据
事件组:
- 一个事件用一bit表示,1表示事件发生了,0表示事件没发生
- 通过按位操作,可以同时处理多个事件的组合,仅传递状态,不携带具体的数据内容。
- 有广播效果:事件或事件的组合发生了,等待它的多个任务都会被唤醒
信号量:
- 核心是"计数值"
- 任务、ISR释放信号量时让计数值加1
- 任务、ISR获得信号量时,让计数值减1
任务通知:
- 核心是存储在任务控制块(TCB)中的一个数值,既可以传递简单的数据,也可以传递状态。
- 该数值会被后续的通知覆盖,仅用来表示传递“最新状态”
- 发通知给谁?必须指定接收任务
- 只能由接收任务本身获取该通知
互斥量:
- 互斥量仅有两种状态:0(解锁状态)和 1(锁定状态),用于表示资源是否被占用。
- 通过上锁(1 变为 0)和解锁(0 变为 1)的方式传递状态。
所有这些内核对象都有如下共性操作:
- 获取(Acquire):任务试图获得某个对象(如队列、信号量、互斥量)。
- 释放(Release):任务在使用完资源后释放对象,以便其他任务可以使用。
- 阻塞与唤醒:
当任务无法获取所需资源时,会进入阻塞状态,等待资源变为可用;当资源可用时,其他任务(或ISR)释放资源会唤醒等待者。 - 超时机制:
在获取资源时,可以设置超时,超时后任务会返回,防止无限等待。
如何选择合适的同步/互斥方法:
- 需要传递复杂数据或消息?
选用 队列。它不仅能传递任意数据,还支持多个数据项的缓存,适合任务之间的消息传递。 - 只需要传递事件状态,不关心具体数据?
选用 事件组。通过设置或清除位来表示事件发生,且能实现类似广播的唤醒效果。 - 需要管理共享资源的数量?
使用 信号量。无论是作为计数信号量还是二值信号量,它能很好地管理资源的可用数量。 - 对性能要求较高,任务之间需要快速传递简单信息?
考虑 任务通知。它开销小,但需要指定接收任务,而且数据可能会被覆盖,适合传递“最新”状态。 - 保护共享资源,确保互斥访问?
使用 互斥量。确保同一时刻只有一个任务能访问临界资源,并且规定了谁获取就必须谁释放的原则。
疑问
疑问1
为什么变量i加上volatile可以让程序变慢一点?
- 使用volatile时,修改变量i时要读、修改、写内存
- 不使用volatle的话,修改变量i时可能会优化:比如把它的值放在CPU寄存器里,累加时不涉及内存,这样速度会加快
- 要访问硬件寄存器的话,要加上volatile避免被优化