相比于普通的互斥锁(Mutex),递归锁(Recursive Mutex) 的核心区别在于它允许“锁的所有者”多次进入而不死锁。为了实现这一点,它必须在底层内存中维护普通锁所没有的额外状态信息。
在 pthread_mutex 或 Swift 的底层实现中,递归锁主要增加并维护以下三个关键状态:
1. 持有者线程 ID (Owner Thread ID)
普通锁只知道“锁是否被占用”(通常是一个 0 或 1 的标记)。而递归锁必须明确记录**“当前是谁锁住了我”**。
-
作用: 当一个线程尝试加锁时,系统会将当前线程的
Thread ID与锁内部记录的Owner ID进行对比。 -
逻辑: * 如果
Thread ID == Owner ID,说明是同一线程重入,直接允许通过。- 如果
Thread ID != Owner ID,则进入标准的竞争/挂起流程。
- 如果
2. 递归计数器 (Recursion Count / Nesting Level)
这是递归锁能够“成对释放”的关键。它记录了当前线程进入了多少层临界区。
- 累加: 每次同一线程成功调用
lock(),计数器 。 - 递减: 每次调用
unlock(),计数器 。 - 真正释放: 只有当计数器归零()时,锁才会清除
Owner Thread ID,并将锁的状态彻底设为“未占用”,从而允许其他线程竞争。
3. 线程局部存储的影子备份 (Optional/Performance Optimization)
在一些高性能实现(如 Objective-C 的 @synchronized)中,为了避免频繁查询内核级别的 Thread ID(这通常涉及系统调用),系统会在 TLS (Thread Local Storage) 中备份一份当前线程持有的锁列表。
- 作用: 线程先检查自己的私有缓存(TLS),如果发现目标锁已经在自己的“持有名单”里,直接在用户态完成计数加减,避开昂贵的内核操作。
4. 递归锁与普通锁的状态对比
| 状态位 | 普通互斥锁 (Normal Mutex) | 递归锁 (Recursive Mutex) |
|---|---|---|
| Locked State | 简单布尔值 (0/1) | 计数器 (Int) |
| Owner ID | 通常不记录(或仅用于调试) | 强制记录 (Thread ID) |
| 加锁逻辑 | 发现已锁定则挂起 | 发现已锁定但 Owner 是自己则继续 |
| 释放逻辑 | 直接设为 0 | 计数减至 0 时才设为 0 |
5. 潜在代价:死锁风险的变种
虽然递归锁解决了“自死锁”,但它引入了另一种风险。因为内部需要维护这些状态,它在性能上比普通锁稍慢。此外,如果你在临界区内忘记了配对调用 unlock(),锁将永远被该线程持有,导致其他所有尝试获取该锁的线程永久阻塞。
注意: 在一些复杂的 C++ 环境中,递归锁内部还会维护一个 “锁类型标记” (Type Flag),用来在运行时区分该锁当前是被配置为
PTHREAD_MUTEX_RECURSIVE还是PTHREAD_MUTEX_ERRORCHECK。