ReaderWriterLockSlim 更适合读多写少场景,因其允许多个线程并发读、仅写时排他,吞吐量可提升3–5倍;但需满足读操作轻量、不嵌套、不滥用升级锁等条件。
ReaderWriterLockSlim 为什么比 lock 更适合读多写少场景
因为 lock 是独占式,哪怕只是读,也会阻塞其他读线程;而 readerwriterlockslim 允许多个线程同时读,只在写时才排他。实际压测中,读并发量大时,吞吐量能提升 3–5 倍——前提是读操作本身不长、不嵌套、不升级锁。
常见错误现象:LockRecursionException(递归获取同一把读锁)、ApplicationDeadlock(写锁未释放、或读锁里调 EnterWriteLock 升级失败)。
- 只在真正「读多写少」且读操作轻量(比如查字典、取缓存值)时用它;别拿它替代
ConcurrentDictionary这类专为并发设计的结构 - 永远用
TryEnterReadLock+try/finally配对释放,避免因异常导致锁未释放 - 不要在持有读锁时调用可能阻塞或外部可重入的代码(比如跨线程回调、UI 更新),否则容易死锁
怎么安全地升级读锁到写锁(EnterUpgradeableReadLock)
EnterUpgradeableReadLock 不是“升级”,而是获得一个可被自己后续转为写锁的读锁。它和普通读锁互斥,但允许多个升级读锁共存——这点常被误解。
典型误用:在 EnterUpgradeableReadLock 后直接调 EnterWriteLock,结果卡死。因为升级读锁本身已阻塞其他写锁,而 EnterWriteLock 又会等所有读锁(包括它自己)释放。
- 必须用
EnterUpgradeableReadLock→ 检查条件 →EnterWriteLock→ 修改 →ExitWriteLock→ExitUpgradeableReadLock - 升级路径必须严格顺序,不能交叉:比如 A 线程持升级读锁,B 线程申请普通读锁,此时 B 会被挂起,直到 A 完成整个流程
- 如果检查后发现无需写,记得立刻
ExitUpgradeableReadLock,否则白白占着升级位
为什么用 TryEnterXXX 而不是 EnterXXX
EnterReadLock 和 EnterWriteLock 是阻塞式,一旦锁被占,线程就挂起;而 TryEnterReadLock(0) 或 TryEnterWriteLock(10) 可设超时,避免无限等待。
真实场景里,数据库连接池耗尽、HTTP 调用超时、缓存穿透都可能让锁等待变成雪崩点。硬等不如快速失败重试或降级。
- 读操作建议用
TryEnterReadLock(1)(1ms 超时),失败则走无锁兜底逻辑(如返回默认值或抛轻量异常) - 写操作可用
TryEnterWriteLock(100),超时后记录日志并返回失败,而不是卡住整个请求线程 - 注意:
TryEnterXXX返回false时,锁一定没拿到,也无需调ExitXXX
Dispose 和静态锁实例的坑
ReaderWriterLockSlim 实现了 IDisposable,但它的资源主要是内部信号量和等待队列。.NET Core 3.0+ 已优化为几乎无非托管资源,但 Dispose() 仍会禁止后续所有锁操作,并抛 ObjectDisposedException。
最容易踩的坑:把锁声明为 static readonly,却在某个模块卸载时调了 Dispose(),结果其他模块还在用,直接崩。
- 除非明确生命周期可控(如单次命令行工具),否则别手动调
Dispose() - 静态锁实例不用
Dispose—— 它随 AppDomain / 进程结束自然清理 - 若用 DI 容器管理,注册为
Singleton即可,别加Dispose调用链
复杂点在于锁的语义边界:它保护的是数据一致性,不是线程安全本身。哪怕锁用对了,如果被保护的对象本身可变(比如 List











