1. 锁排序与死锁避免的核心机制
当看到lock(m1,m2)这样的代码时,很多开发者会本能地产生疑问:这两个锁的获取顺序是随意的吗?是否必须先解锁m1才能锁m2?这实际上触及了并发编程中最危险的陷阱之一——死锁问题。让我们用一个真实场景来理解:想象两个线程A和B,A先锁m1再尝试锁m2,而B先锁m2再尝试锁m1,当两者同时执行时就会形成互相等待的僵局。
在C++中,std::lock(m1, m2)这个函数调用实际上采用了死锁避免算法(Deadlock Avoidance Algorithm)。它的精妙之处在于:无论你以什么顺序传入参数,内部都会通过某种策略(如锁排序)确保所有锁的获取是原子化的。具体实现可能采用类似银行家算法的预检机制,或者更常见的锁层次结构(Lock Hierarchy)。
关键提示:手动按固定顺序获取多个锁(如总是先m1后m2)确实能避免死锁,但这依赖于所有开发人员严格遵守约定。而
std::lock通过机械性强制排序,消除了人为失误的可能。
2. 锁排序的底层实现原理
现代操作系统和标准库通常采用两种主要策略实现锁排序:
2.1 地址排序法
这是最直观的实现方式。当std::lock(m1, m2)执行时:
- 比较两个锁对象的内存地址
- 总是先锁定地址较小的那个锁
- 再尝试锁定地址较大的锁
- 如果中途任何锁获取失败,会释放已持有的锁
这种方法的优势在于:
- 完全 deterministic(确定性)
- 不依赖锁的任何外部属性
- 适用于任意数量的锁
cpp复制// 伪代码示例
void std::lock(Lock& l1, Lock& l2) {
if (&l1 < &l2) {
l1.lock();
l2.lock();
} else {
l2.lock();
l1.lock();
}
}
2.2 尝试回退法
更健壮的实现会结合try-lock机制:
- 同时尝试获取所有锁
- 如果只能获取部分锁:
- 释放已获得的锁
- 随机等待一段时间
- 重试整个过程
- 直到所有锁都获取成功
这种方法在高度竞争环境下表现更好,但实现复杂
