6 同步、互斥与通信总结
同步、互斥与通信总结
在 RTOS 中,多任务并发运行是系统的基本特征。多个任务在执行过程中,往往既需要按照一定顺序协作,又会竞争同一个共享资源,还需要彼此传递数据或事件。因此,就引出了三个核心概念:同步、互斥、通信。
1 基本概念
1.1 什么是同步
同步强调的是任务之间的执行先后关系。
也就是说,一个任务必须等待另一个任务完成某件事之后,自己才能继续执行。
例如:
- 同事 A 先写完报表,经理 B 才能拿去汇报
- 中断接收完数据,任务才能开始处理
- DMA 传输完成后,任务才能继续后续流程
这些都属于同步问题,本质上是在解决:
谁先做,谁后做,后者要不要等前者。
1.2 什么是互斥
互斥强调的是共享资源在同一时刻只能被一个任务占用。
如果多个任务同时访问同一个资源,就可能导致数据混乱或系统异常。
例如:
- 两个任务同时使用串口打印,输出会交叉混乱
- 两个任务同时操作 LCD,显示内容可能错乱
- 多个任务同时访问同一个全局变量,可能造成数据竞争
这类问题本质上是在解决:
同一时刻谁能用资源,谁必须等待。
1.3 什么是通信
通信强调的是任务之间传递数据、状态或事件。
任务之间并不是孤立运行的,它们常常要交换信息来实现协作。
例如:
- 一个任务采集到数据后发给另一个任务处理
- 中断通知任务“数据到了”
- 某任务告诉其他任务“某个事件发生了”
因此,通信本质上是在解决:
任务之间如何交换信息。
2 同步与互斥的关系
同步和互斥经常放在一起讲,因为二者关系非常密切。
- 同步关注执行顺序
- 互斥关注资源独占
二者最典型的联系是:
互斥往往可以通过同步机制来实现。
例如“我等你用完厕所,我再进去”:
- “等你用完”体现了同步
- “同一时间只能一个人用厕所”体现了互斥
所以可以说:
互斥是目标,同步是实现手段之一。
3 临界资源
在系统中,那些同一时刻只能被一个任务访问的资源,称为临界资源。
常见临界资源有:
- 串口
- LCD
- 打印设备
- 文件系统
- 某些外设寄存器
- 共享内存、共享变量
如果多个任务同时访问临界资源,就可能出现竞争,导致:
- 输出内容交叉
- 数据被覆盖
- 状态不一致
- 程序行为异常
因此,临界资源必须通过互斥机制进行保护。
4 为什么同步与互斥并不简单
在裸机程序中,人们常用一个全局变量或静态变量表示资源是否可用,比如:
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 判断
bCanUse == 1 - 还没来得及执行
bCanUse = 0,就被切换出去了 - 任务 B 运行,也判断
bCanUse == 1 - 于是 A、B 都进入了临界区
这样就失去了互斥效果。
5 为什么简单的减一操作也不可靠
有人会想到改进:
不先判断,而是先对变量减一:
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-- 在底层并不是一条不可分割的原子操作,而可能分成多条机器指令,例如:
LDR R0, [bCanUse]
DEC R0, #1
STR R0, [bCanUse]如果任务 A 在读取变量后被切换出去,任务 B 也可能读到相同的旧值,从而导致两个任务都进入临界区。
这说明:
只要“检查和修改”这个过程会被打断,就可能产生竞争条件。
6 为什么需要原子操作或内核对象
要想真正实现可靠的同步与互斥,关键在于:
对共享状态的检查与修改必须是原子的,也就是不可分割、不可中断的。
一种底层做法是:
- 关闭中断
- 执行关键操作
- 再打开中断
例如:
disable_irq();
if (bCanUse)
{
bCanUse = 0;
enable_irq();
/* 使用 LCD */
bCanUse = 1;
return 0;
}
enable_irq();
return -1;通过关闭中断,可以避免在关键区被打断。
但这只是底层保护手段,在实际 RTOS 编程中,更推荐直接使用操作系统提供的内核对象来完成同步、互斥与通信。
这些内核对象包括:
- 任务通知(Task Notification)
- 队列(Queue)
- 事件组(Event Group)
- 信号量(Semaphore)
- 互斥量(Mutex)
7 这些内核方法的共同点
这些内核对象虽然功能不同,但操作方式有很多共同点,通常都支持:
- 获取
- 释放
- 阻塞等待
- 唤醒任务
- 超时返回
典型流程如下:
- 任务 A 尝试获取资源
- 如果成功,就继续执行
- 如果失败,就进入阻塞状态
- 任务 B 释放资源后,内核唤醒 A
- 或者 A 等待超时,自行返回
因此,学习这些机制时,不要只记接口名,而要抓住它们真正传递的内容:
到底是在传数据、传事件、传资源数量,还是在保护临界区。
8 各类方法的对比思路
区分队列、事件组、信号量、任务通知、互斥量时,可以抓住以下几个问题:
8.1 能不能传数据
- 队列:可以传真正的数据
- 任务通知:可以传简单数据或状态
- 事件组:不能传数据,只能传事件状态
- 信号量:不能传具体数据,本质上传递的是数量
- 互斥量:不传数据,只表示锁的状态
8.2 发送者和接收者是否受限
- 队列:谁都可以发,谁都可以收
- 事件组:谁都可以设置事件,谁都可以等待事件
- 信号量:谁都可以释放,谁都可以获取
- 任务通知:发送者不限,但接收者必须是指定任务
- 互斥量:谁获取了锁,最后通常就必须由谁释放
8.3 是一对一还是广播
- 队列:通常一个数据只唤醒一个接收者
- 信号量:一个资源通常只唤醒一个等待者
- 事件组:可以同时唤醒多个等待相同事件的任务,具有广播效果
- 任务通知:典型 N 对 1
- 互斥量:一次只允许一个任务获得访问权
8.4 偏通信还是偏互斥
- 队列:偏通信
- 事件组:偏同步和事件通知
- 信号量:偏同步、资源计数
- 任务通知:偏轻量级点对点通知
- 互斥量:偏互斥保护
9 各类方法总结表
| 内核对象 | 生产者 | 消费者 | 传递内容 | 主要用途 | 典型特点 |
|---|---|---|---|---|---|
| 队列 | ALL | ALL | 数据,可存多个数据项 | 任务间通信 | 谁都能发,谁都能收,一个数据通常唤醒一个接收者 |
| 事件组 | ALL | ALL | 事件位、事件组合 | 事件同步 | 用 bit 表示事件,可广播唤醒多个任务 |
| 信号量 | ALL | ALL | 计数值 | 同步、资源计数 | 释放加 1,获取减 1 |
| 任务通知 | ALL | 指定任务 | 数据或状态 | 定向通知 | N 对 1,效率高,必须指定接收任务 |
| 互斥量 | 获取者持有 | 持有者释放 | 锁状态 0/1 | 临界资源保护 | 谁加锁谁解锁,专门用于互斥 |
10 各类方法分别理解
10.1 队列
队列本质上是一个数据缓冲区。
特点:
- 可以存多个数据
- 可以传递真正的数据内容
- 发送者、接收者都没有严格限制
- 适合“一个任务生产数据,另一个任务消费数据”的场景
例如:
- 按键任务把键值送给处理任务
- 串口接收任务把数据交给解析任务
- 日志任务从队列中取出待打印信息
一句话概括:
队列的核心是传数据。
10.2 事件组
事件组本质上是一组 bit 位状态。
特点:
- 每一位表示一个事件是否发生
- 可以等待某一位,也可以等待多个位的组合
- 可以实现“与”“或”条件判断
- 不能传具体数据
- 具有广播效果,多个任务都可能被唤醒
例如:
- 等待 “WiFi 已连接”
- 等待 “串口接收完成”
- 等待 “事件 A 和事件 B 都发生”
一句话概括:
事件组的核心是传事件状态。
10.3 信号量
信号量本质上是一个计数器。
特点:
- 释放信号量:计数值加 1
- 获取信号量:计数值减 1
- 当计数值为 0 时,获取者可能阻塞
- 适合表示资源数量,也可用于同步
例如:
- 中断告诉任务“数据到了”
- 系统中有 3 个相同资源可供使用
- 某个任务完成后给另一个任务一个同步信号
一句话概括:
信号量的核心是传数量。
10.4 任务通知
任务通知本质上是:
利用任务控制块 TCB 中的通知值,直接给某个指定任务发消息。
特点:
- 必须指定接收任务
- 发送者可以很多,但接收者只能是那一个任务
- 可以传简单数据,也可以传状态
- 通常效率较高,开销较小
- 通知值可能被覆盖,不适合复杂消息排队
例如:
- 中断完成后直接通知某个处理任务
- 一个任务只需要唤醒另一个固定任务
- 某个专门任务等待硬件事件到来
一句话概括:
任务通知的核心是定向通知。
10.5 互斥量
互斥量本质上是一个带所有权的锁。
特点:
- 数值通常只有 0 和 1
- 谁获得了互斥量,谁就必须释放它
- 专门用于保护临界资源
- 不用于传递数据
- 更强调“资源独占访问”
例如:
- 保护 LCD
- 保护串口
- 保护共享缓冲区
- 保护文件系统接口
一句话概括:
互斥量的核心是独占上锁。
11 这些方法最核心的区别
可以用最简单的方式来记:
- 队列:传数据
- 事件组:传事件
- 信号量:传数量
- 任务通知:定向通知
- 互斥量:独占上锁
再换一种说法:
- 需要任务间传输消息、命令、数据块,用 队列
- 需要表示某个事件是否发生,或多个事件是否同时满足,用 事件组
- 需要表示资源个数,或完成一次简单同步,用 信号量
- 需要高效地通知某个固定任务,用 任务通知
- 需要保护临界资源,防止同时访问,用 互斥量
各种方法对比图

12 如何选择
在实际开发中,可以按下面的思路选择:
| 场景 | 推荐方法 |
|---|---|
| 任务之间传输数据、消息、命令 | 队列 |
| 需要等待一个或多个事件发生 | 事件组 |
| 需要表示资源数量或进行简单同步 | 信号量 |
| 只想通知某个固定任务 | 任务通知 |
| 保护共享资源、临界区 | 互斥量 |
13 最终总结
“同步、互斥与通信”是 RTOS 多任务系统中最基础、最核心的内容。
- 同步解决任务之间的先后协作问题
- 互斥解决共享资源的独占访问问题
- 通信解决任务之间的数据和事件传递问题
在裸机程序中,可以尝试用全局变量、静态变量甚至关闭中断来实现简单保护;
但在 RTOS 中,由于任务调度和中断会导致并发竞争,普通变量方式并不可靠。
因此,实际开发中通常应优先使用 RTOS 提供的内核对象:
- 队列负责传递数据
- 事件组负责传递事件状态
- 信号量负责同步和资源计数
- 任务通知负责高效定向通知
- 互斥量负责保护临界资源
掌握这些对象的区别与适用场景,就是掌握 RTOS 多任务协作的基础。
