Monitor 和默认 lock 不是公平锁,因其底层依赖临界区或同步块,不保证等待顺序与获取顺序一致,可能导致线程饥饿;SemaphoreSlim(1,1,true) 是最轻量的公平锁实现,需注意版本兼容性与性能权衡。
Monitor 和默认 lock 不是公平锁Monitor.Enter(即 C# 的 lock 语句)底层依赖 Windows 的临界区或 CLR 的同步块,**不保证线程获取锁的顺序与等待顺序一致**。多个线程竞争时,可能刚唤醒的线程被新来的线程“插队”,导致某些线程长期饥饿。这不是 bug,而是为吞吐量做的权衡。
Monitor.TryEnter(int) 超时返回 false 后,线程需自行重试,但重试时机无法对齐排队位置SemaphoreSlim 手动构造公平锁SemaphoreSlim 在 count = 1 且启用 fairness: true 时,内部使用 FIFO 等待队列(自 .NET Core 2.0+ / .NET 5+),是最轻量、最贴近需求的公平锁实现方式。
true: new SemaphoreSlim(1, 1, true);省略第三个参数或传 false 就退化为非公平模式Wait() 会阻塞直到获得信号,WaitAsync() 支持取消和异步等待Release(),否则锁永久泄露 —— 建议用 try/finally 或 using(需封装为可释放包装类)var fairLock = new SemaphoreSlim(1, 1, true);// 获取锁(阻塞式) fairLock.Wait(); try { // 临界区操作 } finally { fairLock.Release(); }
Fa
irLock 类封装更安全的 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%。
SemaphoreSlim 的 fairness 参数(会忽略),必须升级到 .NET Core 2.0+ 或 .NET 5+Monitor 和 SemaphoreSlim 实现同一逻辑真正需要公平锁的场景其实很少——多数时候是诊断出明确的饥饿问题后才引入。先确认争用模式,再决定是否值得为公平性牺牲一点吞吐。
来电咨询