ReaderWriterLockSlim 因允许多读单写、用户态实现轻量,比 lock 和 Monitor 更适合读多写少场景;但不支持递归(默认)和跨 await 持锁,需手动释放锁并注意锁粒度与状态安全。

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_cache = new List (); 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(),否则锁泄漏,最终导致所有线程卡死
真正难的不是调用几个方法,而是判断哪段代码该进读锁、哪段该进写锁、以及有没有漏掉边界条件下的状态不一致风险。










