ReaderWriterLockSlim 为什么比 lock 和 Monitor 更适合读多写少场景
因为
ReaderWriterLockSlim允许多个线程同时读、但写时独占,而
lock无论读写都串行。在缓存、配置、只读集合等读远多于写的场景下,它能显著提升并发吞吐量。
注意:它不是
ReaderWriterLock的简单升级版——后者已标记为过时,且内部使用事件内核对象,开销大;
ReaderWriterLockSlim是用户态实现,轻量,但不支持递归获取(除非显式开启)。 默认不支持同一线程重复进入读锁(
EnterReadLock调用两次会死锁),需构造时传入
LockRecursionPolicy.SupportsRecursion不支持跨 await 边界持有锁(即不能在
async方法中
await前加锁、await 后解锁),否则会抛出
SynchronizationLockException写锁优先级高于读锁:一旦有线程调用
EnterWriteLock,后续的
EnterReadLock会被阻塞,直到写锁释放
正确初始化和基础读/写模式
声明时推荐使用
new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion)(默认值),除非你明确需要递归读锁;避免用无参构造函数,防止未来行为变化。
典型用法是配合
try/finally确保解锁,因为
Dispose()不会自动释放锁,必须手动调用
ExitReadLock()或
ExitWriteLock()。
private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
private List<string> _cache = new List<string>();
public string ReadFirst()
{
_rwLock.EnterReadLock();
try
{
return _cache.FirstOrDefault();
}
finally
{
_rwLock.ExitReadLock();
}
}
public void AddItem(string item)
{
_rwLock.EnterWriteLock();
try
{
_cache.Add(item);
}
finally
{
_rwLock.ExitWriteLock();
}
}
如何安全处理超时与取消
EnterReadLock(int millisecondsTimeout)和
EnterWriteLock(int millisecondsTimeout)支持超时,返回
bool表示是否成功获取锁。超时后不要假设锁已被获取,也不能调用
ExitXxxLock()。
没有原生 CancellationToken 支持,但可通过
SpinWait+ 循环轮询 +
IsCancellationRequested模拟(不推荐高频轮询)。更实际的做法是:设置合理超时(如 100–500ms),捕获
TimeoutException后降级或重试。 超时值设为
-1等价于无限等待(同无参版本) 设为
0表示“仅尝试一次”,立即返回
false若锁不可用 不要在高竞争场景下依赖长超时,容易引发请求堆积
常见误用与性能陷阱
最常被忽略的是锁粒度问题:把整个方法体包在
EnterWriteLock里,却在锁内做了 IO、远程调用或复杂计算,导致其他读写线程长时间阻塞。
另一个隐蔽问题是“读锁中修改共享状态”——看似只读,实则调用了可能改变内部状态的属性或方法(例如
List.Count安全,但
ObservableCollection.Count可能触发通知)。 写锁中尽量只做内存操作;耗时逻辑(如文件写入、HTTP 请求)应移出锁外,先计算好结果再进锁更新字段 避免在读锁中调用未审查的第三方方法,尤其涉及事件触发、数据绑定或 LINQ ToObjects 的
ToList()等可能隐式修改源集合的操作
TryEnterReadLock和
TryEnterWriteLock返回
true后,必须配对调用对应
ExitXxxLock(),否则锁泄漏,最终导致所有线程卡死
真正难的不是调用几个方法,而是判断哪段代码该进读锁、哪段该进写锁、以及有没有漏掉边界条件下的状态不一致风险。
