← Back to Blog
EN中文

公平锁与递归锁的混合设计

在并发编程中,锁是保护共享资源的基本手段。然而,标准锁有两个常见问题:

  1. 递归问题:同一线程多次获取非递归锁会死锁
  2. 公平性问题:非公平锁可能导致线程饥饿

如何在保证线程安全的同时,解决这两个问题?本文将深入分析工业级代码中的公平锁与递归锁混合设计,并用 Rust 进行净室重构演示。

从问题说起:锁的两难

递归锁的困境

// 非递归锁:同一线程重复获取会死锁
std::mutex m;
m.lock();
m.lock(); // 死锁!

公平锁的困境

// 非公平锁:新请求可能插队
// 线程 A 获取锁
// 线程 B、C 等待
// 线程 A 释放锁,B 获取
// 线程 A 再次获取锁(快速释放快速获取)
// B 永远拿不到锁 -> 饥饿

工业级解法:递归 + 公平

原始代码中使用了 TRecursiveFairLock

class TRecursiveFairLock {
public:
    void Acquire() noexcept;
    bool TryAcquire() noexcept;
    void Release() noexcept;
    
    bool IsFree() noexcept;
    bool IsOwnedByMe() noexcept;
    
private:
    class TImpl;
    THolder<TImpl> Impl;
};

这种设计的核心思想是:

  • 递归支持:维护 recursion_count,同一线程可重入
  • 公平队列:FIFO 顺序,防止线程饥饿
  • Pimpl 模式:隐藏实现细节,保持头文件简洁

权衡分析

优势

  1. 避免死锁:递归特性允许同一线程多次获取锁
  2. 防止饥饿:公平队列确保等待线程按顺序获取
  3. 接口简洁:通过 Pimpl 隐藏复杂实现

代价

  1. 实现复杂度:递归 + 公平增加锁的实现复杂度
  2. 性能开销:公平队列维护有额外开销
  3. 内存使用:Impl 指针增加内存开销

Rust 净室演示

struct FairRecursiveLock {
    inner: Mutex<FairRecursiveLockInner>,
}

struct FairRecursiveLockInner {
    owner_thread_id: Option<ThreadId>,
    recursion_count: usize,
    wait_queue: Vec<WaitNode>,
}

impl FairRecursiveLock {
    fn lock(&self) {
        let current_thread = thread::current().id();
        let mut inner = self.inner.lock().unwrap();
        
        // 情况 1:锁空闲
        if inner.owner_thread_id.is_none() {
            inner.owner_thread_id = Some(current_thread);
            inner.recursion_count = 1;
            return;
        }
        
        // 情况 2:同一线程重入
        if inner.owner_thread_id == Some(current_thread) {
            inner.recursion_count += 1;
            return;
        }
        
        // 情况 3:需要等待...
    }
}

fn main() {
    let lock = Arc::new(FairRecursiveLock::new());
    
    // 测试递归获取
    lock.lock();
    lock.lock();
    lock.unlock();
    lock.unlock();
}

总结

本文深入分析了公平锁与递归锁的混合设计,探讨了以下核心权衡:

  1. 递归 vs 非递归:用额外计数器换取重入能力
  2. 公平 vs 非公平:用吞吐量换取确定性
  3. 复杂度 vs 可维护性:用 Pimpl 隐藏实现细节

这种设计模式在需要高并发和高确定性的系统中非常常见,理解其背后的权衡对于设计可靠的并发系统至关重要。