c# C# 中如何实现一个公平锁(Fair Lock)

13次阅读

Monitor 和默认 lock 不是公平锁,因其底层依赖临界区或同步块,不保证等待顺序与获取顺序一致,可能导致线程饥饿;SemaphoreSlim(1,1,true) 是最轻量的公平锁实现,需注意版本兼容性与性能权衡。

c# C# 中如何实现一个公平锁(Fair Lock)

为什么 Monitor 和默认 lock 不是公平锁

Monitor.Enter(即 C# 的 lock 语句)底层依赖 windows 的临界区或 CLR 的同步块,**不保证线程获取锁的顺序与等待顺序一致**。多个线程竞争时,可能刚唤醒的线程被新来的线程“插队”,导致某些线程长期饥饿。这不是 bug,而是为吞吐量做的权衡。

  • 没有 FIFO 队列机制,调度由 OS 决定,不可控
  • Monitor.tryEnter(int) 超时返回 false 后,线程需自行重试,但重试时机无法对齐排队位置
  • 即使在高争用下观察到“看似有序”,也不能当作公平性保障

SemaphoreSlim 手动构造公平锁

SemaphoreSlimcount = 1 且启用 fairness: true 时,内部使用 FIFO 等待队列(自 .net Core 2.0+ / .NET 5+),是最轻量、最贴近需求的公平锁实现方式。

  • 构造时必须传 truenew SemaphoreSlim(1, 1, true);省略第三个参数或传 false 就退化为非公平模式
  • Wait() 会阻塞直到获得信号,WaitAsync() 支持取消和异步等待
  • 务必配对调用 Release(),否则锁永久泄露 —— 建议用 try/finallyusing(需封装为可释放包装类)
var fairLock = new SemaphoreSlim(1, 1, true); 

// 获取锁(阻塞式) fairLock.Wait(); try { // 临界区操作 } finally { fairLock.Release(); }

自定义 FairLock 类封装更安全的 API

直接暴露 SemaphoreSlim 容易漏掉 Release(),也缺乏语义表达。封装一层能强制资源管理,并隐藏公平性细节。

  • 实现 IDisposable,支持 using 语法糖
  • 构造函数只接受 fairness: true,避免误用非公平实例
  • 内部用 WaitAsync + CancellationToken 更适合现代异步场景
  • 注意:不要在 Dispose() 中调用异步方法,Release() 是同步的
public sealed class FairLock : IDisposable {     private readonly SemaphoreSlim _semaphore; 
public FairLock() => _semaphore = new SemaphoreSlim(1, 1, true);  public async ValueTask AcquireAsync(CancellationToken ct = default) {     await _semaphore.WaitAsync(ct);     return new Releaser(_semaphore); }  private struct Releaser : IDisposable {     private readonly SemaphoreSlim _semaphore;     public Releaser(SemaphoreSlim s) => _semaphore = s;     public void Dispose() => _semaphore.Release(); }  public void Dispose() => _semaphore?.Dispose();

}

使用示例:

var lockObj = new FairLock(); 

await using (await lockObj.AcquireAsync()) { // 临界区 }

性能与兼容性注意事项

公平锁天然比非公平锁开销大:每次释放都要唤醒队首线程,且需维护等待队列节点。在低争用场景几乎无感,但在高频短临界区(如计数器递增)中,吞吐量可能下降 20–40%。

  • .NET Framework 4.7.2 及更早版本不支持 SemaphoreSlimfairness 参数(会忽略),必须升级到 .NET Core 2.0+ 或 .NET 5+
  • 若需跨平台一致性,避免混用 MonitorSemaphoreSlim 实现同一逻辑
  • 公平性只作用于“等待中的线程”,已进入临界区的线程不受影响;不要指望它解决死锁或嵌套锁顺序问题

真正需要公平锁的场景其实很少——多数时候是诊断出明确的饥饿问题后才引入。先确认争用模式,再决定是否值得为公平性牺牲一点吞吐。

text=ZqhQzanResources