鸿蒙南向开发教程 Day 7 附录:互斥锁深度解析
互斥锁(Mutual Exclusion Lock)是一种用于保护临界资源的同步机制,确保同一时刻只有一个线程可以访问被保护的资源。│ 临界资源(Critical Resource) ││ 全局变量、共享缓冲区、硬件寄存器等 ││ │ │ │ ││ ↓ ││ │ [Locked] │ ← 只有一个 ││ │ [Unlocked] │ 线程通过 │要点内容核心功能保护临界资源,确保独占访问关键特性所
·
目标:深入理解互斥锁的设计原理、使用场景、常见问题及解决方案
一、什么是互斥锁(Mutex)?
1.1 核心概念
互斥锁(Mutual Exclusion Lock) 是一种用于保护临界资源的同步机制,确保同一时刻只有一个线程可以访问被保护的资源。
┌─────────────────────────────────────────┐
│ 临界资源(Critical Resource) │
│ 全局变量、共享缓冲区、硬件寄存器等 │
├─────────────────────────────────────────┤
│ ┌─────────┐ ┌─────────┐ ┌─────┐ │
│ │ Thread1 │ │ Thread2 │ │ ... │ │
│ └────┬────┘ └────┬────┘ └──┬──┘ │
│ │ │ │ │
│ └──────────────┼────────────┘ │
│ ↓ │
│ ┌─────────────┐ │
│ │ Mutex │ │
│ │ [Locked] │ ← 只有一个 │
│ │ [Unlocked] │ 线程通过 │
│ └─────────────┘ │
└─────────────────────────────────────────┘
1.2 为什么叫"互斥"?
- 互:相互排斥
- 斥:排斥其他线程
- 锁:锁定资源,禁止他人访问
本质:对临界资源的访问权进行排队管理。
二、互斥锁 vs 其他同步机制
| 机制 | 核心功能 | 使用场景 | 特点 |
|---|---|---|---|
| Mutex(互斥锁) | 资源独占访问 | 保护共享变量、数据结构 | 所有权、递归、优先级继承 |
| Semaphore(信号量) | 资源计数管理 | 控制并发数量、生产者-消费者 | 无所有权、可多个线程获取 |
| Event Flags(事件标志) | 事件通知同步 | 多条件组合等待、任务触发 | 32位标志位、AND/OR等待 |
| RWLock(读写锁) | 读共享写独占 | 读多写少场景 | 读并发、写互斥 |
关键区别:Mutex 有所有权,Semaphore 没有
- Mutex:只有获取锁的线程才能释放锁
- Semaphore:任何线程都可以释放(增加计数),不记录谁获取的
三、互斥锁的核心特性
3.1 所有权(Ownership)
// Thread A 获取锁
osMutexAcquire(mid, osWaitForever); // Thread A 成为 Owner
// 只有 Thread A 能释放
osMutexRelease(mid); // ✅ 正确
// Thread B 尝试释放
osMutexRelease(mid); // ❌ 错误!返回 osErrorResource
设计目的:防止误释放,确保锁的配对使用。
3.2 递归锁定(Recursive Locking)
void func_a(osMutexId_t mid)
{
osMutexAcquire(mid, osWaitForever); // 第1次获取,计数=1
// ... 访问资源 ...
func_b(mid); // 调用 func_b
osMutexRelease(mid); // 计数减到0,真正释放
}
void func_b(osMutexId_t mid)
{
osMutexAcquire(mid, osWaitForever); // 第2次获取,同一线程,计数=2
// ... 访问资源 ...
osMutexRelease(mid); // 计数减到1,未真正释放
}
递归锁特性:
- 同一线程可多次获取同一锁
- 获取次数 = 释放次数 时才真正解锁
- 防止同一线程内部死锁
3.3 优先级继承(Priority Inheritance)
优先级反转问题:
高优先级线程 H(优先级 3)
中优先级线程 M(优先级 5)
低优先级线程 L(优先级 7),持有 Mutex
执行顺序:
1. L 获取锁,开始执行
2. H 就绪,抢占 CPU,尝试获取锁 → 阻塞,等待 L 释放
3. M 就绪,优先级比 L 高,抢占 CPU
4. M 执行完毕,L 继续执行,释放锁
5. H 终于获取锁,开始执行
问题:H 被 M 间接阻塞!M 不需要锁,却阻碍了 H。
这叫"优先级反转"——高优先级线程被低优先级线程阻塞。
优先级继承解决方案:
当 H 阻塞等待 L 的锁时:
L 的临时优先级提升到 H 的级别(优先级 3)
执行顺序变为:
1. L 获取锁,开始执行(优先级 7)
2. H 就绪,尝试获取锁 → 阻塞
3. L 的优先级提升到 3(继承 H 的优先级)
4. M 就绪,但优先级 5 < L 的 3,无法抢占
5. L 执行完毕,释放锁,优先级恢复 7
6. H 获取锁,立即执行
7. H 执行完毕,M 执行
结果:M 无法阻碍 H,优先级反转消除!
LiteOS-M 的互斥锁默认支持优先级继承。
四、互斥锁的使用规范
4.1 黄金法则
| 法则 | 说明 |
|---|---|
| 成对使用 | 每个 Acquire 必须有对应的 Release |
| 最小临界区 | 锁保护的代码越短越好,减少竞争 |
| 禁止阻塞操作 | 临界区内不能调用 osDelay、I/O 等阻塞函数 |
| 避免嵌套过多 | 嵌套锁增加死锁风险,设计时尽量减少 |
| 先获取后释放顺序 | 多个锁时,所有线程按相同顺序获取 |
4.2 错误示例:死锁
osMutexId_t m1, m2;
// Thread A
void thread_a(void)
{
osMutexAcquire(m1, osWaitForever); // 获取 m1
osDelay(1); // 模拟工作
osMutexAcquire(m2, osWaitForever); // 尝试获取 m2 → 阻塞,等待 B 释放 m2
// ... 永远不会执行到这里 ...
osMutexRelease(m2);
osMutexRelease(m1);
}
// Thread B
void thread_b(void)
{
osMutexAcquire(m2, osWaitForever); // 获取 m2
osDelay(1);
osMutexAcquire(m1, osWaitForever); // 尝试获取 m1 → 阻塞,等待 A 释放 m1
// ... 永远不会执行到这里 ...
osMutexRelease(m1);
osMutexRelease(m2);
}
// 结果:A 等 m2,B 等 m1,互相等待,永久阻塞 = 死锁!
死锁条件(Coffman 四条件):
- 互斥:资源独占
- 持有并等待:持有锁同时等待其他锁
- 不可抢占:锁不能被强制释放
- 循环等待:形成等待环路
预防方法:所有线程按相同顺序获取锁(如都先 m1 后 m2)。
4.3 正确示例:成对使用 + 最小临界区
void correct_usage(osMutexId_t mid)
{
// 准备数据(不需要锁)
int local_data = compute_something();
// 获取锁
osMutexAcquire(mid, osWaitForever);
// 临界区:只放必要的共享资源访问
g_shared_value += local_data;
g_shared_count++;
// 立即释放
osMutexRelease(mid);
// 后续处理(不需要锁)
printf("Done\n");
}
五、互斥锁在 OpenHarmony 中的实现细节
5.1 数据结构
LiteOS-M 互斥锁控制块(简化):
typedef struct {
UINT8 ucMuxStat; // 锁状态:OS_MUX_USED / OS_MUX_UNUSED
UINT16 usMuxCount; // 递归计数(递归锁次数)
UINT32 uxMuxID; // 锁 ID
LOS_DL_LIST stMuxList; // 等待队列(阻塞的线程链表)
LOS_TASK_CB *pstOwner; // 所有者任务控制块指针
UINT16 usPriority; // 所有者原始优先级(用于优先级恢复)
} LOS_MUX_CB;
5.2 状态转换图
┌─────────┐ osMutexNew() ┌─────────┐ osMutexAcquire() ┌─────────┐
│ 未创建 │ ──────────────→ │ 空闲 │ ────────────────────→ │ 锁定 │
│ (NULL) │ │(Unlocked)│ 无竞争:立即获取 │ (Locked)│
└─────────┘ └─────────┘ 有竞争:加入等待队列 └────┬────┘
↑ │
└────────────────────────────────────┘
osMutexRelease()
唤醒等待队列首个线程
或变为空闲
5.3 等待队列管理
等待队列(优先级排序):
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
│ T3 │ → │ T5 │ → │ T7 │ → │ T9 │ → NULL
│ prio│ │ prio│ │ prio│ │ prio│
│ 3 │ │ 5 │ │ 7 │ │ 9 │
└─────┘ └─────┘ └─────┘ └─────┘
最高优先级 最低优先级
释放锁时:唤醒 T3(队首,优先级最高)
LiteOS-M 使用优先级队列管理等待线程,确保高优先级线程优先获取锁。
六、常见问题排查
| 现象 | 原因 | 排查方法 |
|---|---|---|
| 程序卡死 | 死锁 | 检查 osMutexGetOwner,看哪个线程持有锁不释放 |
| 数据异常 | 未加锁保护 | 检查所有访问共享变量的位置是否都有 Acquire/Release |
| 优先级反转 | 高优先级线程被低优先级阻塞 | 确认 LiteOS 优先级继承是否启用 |
| 返回 osErrorResource | 非所有者释放 | 确认释放线程与获取线程是否为同一个 |
| 递归获取失败 | 超出最大递归深度 | 检查递归调用层次,或改用普通锁 |
七、总结
| 要点 | 内容 |
|---|---|
| 核心功能 | 保护临界资源,确保独占访问 |
| 关键特性 | 所有权、递归锁定、优先级继承 |
| 使用规范 | 成对使用、最小临界区、禁止阻塞操作 |
| 死锁预防 | 统一获取顺序、超时机制、避免嵌套 |
| 底层实现 | LiteOS-M LOS_MUX_CB 控制块 + 优先级等待队列 |
| 与信号量区别 | Mutex 有所有权,Semaphore 无所有权 |
八、下一步
Day 8 预告:信号量(Semaphore) —— 资源计数与多任务同步。
需要继续 Day 8 的内容吗?
更多推荐
所有评论(0)