目标:深入理解互斥锁的设计原理、使用场景、常见问题及解决方案


一、什么是互斥锁(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 四条件)

  1. 互斥:资源独占
  2. 持有并等待:持有锁同时等待其他锁
  3. 不可抢占:锁不能被强制释放
  4. 循环等待:形成等待环路

预防方法:所有线程按相同顺序获取锁(如都先 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 的内容吗?

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐