什么是读写者问题的 C# 实现难点
读写者问题不是 .NET 内置的同步原语,
ReaderWriterLockSlim是最接近、也最常被误用的方案——它默认不保证写优先,且读线程饥饿时不会自动让写线程插队。很多开发者直接套用
EnterReadLock()/
EnterWriteLock()就以为解决了,结果在高并发读+偶发写的场景下,写操作被无限推迟。
用 ReaderWriterLockSlim
实现写优先(避免写饥饿)
必须显式启用写优先模式,否则读线程只要持续到来,写线程永远等不到机会。关键在于构造时传入
LockRecursionPolicy.NoRecursion并设置
UseSpinWait = true提升响应,但核心是调用
EnterUpgradeableReadLock()+
EnterWriteLock()组合来模拟“检查-升级”逻辑。
var rwLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
// 写操作(带超时防死锁)
bool acquired = false;
try
{
acquired = rwLock.TryEnterWriteLock(1000); // 1秒超时
if (!acquired) throw new TimeoutException("Write lock timeout");
<pre class='brush:php;toolbar:false;'>// 执行写入...} finally { if (acquired) rwLock.ExitWriteLock(); }
不要用EnterWriteLock()无参版本——可能无限阻塞 读操作可用
TryEnterReadLock(int)配合重试,但读多写少时建议直接用
EnterReadLock()升级路径(读→写)必须通过
EnterUpgradeableReadLock(),不能先读再抢写锁,否则引发死锁
手动实现公平读写锁(需要严格 FIFO 调度)
当
ReaderWriterLockSlim的“写优先”仍不够用(比如要求第 N 个写请求必须在前 N−1 个写完成后再执行),就得用
ConcurrentQueue<task></task>+
SemaphoreSlim手动编排。本质是把读/写请求转为任务,由单一线程调度器按入队顺序分发。
典型结构:
一个SemaphoreSlim控制「当前是否允许新读」(初始值 1) 一个
int _activeReaders计数器 +
object _readLock保护它 所有写请求先入队,写任务执行前先
WaitAsync()等待读计数归零 每个读任务执行前先
WaitAsync()获取读许可,完成后释放
这种实现在吞吐量上不如
ReaderWriterLockSlim,但能确保写请求不被读流淹没——适合配置更新、状态切换等强时效性场景。
常见错误:混用锁与 async/await
ReaderWriterLockSlim不支持异步等待。下面这段代码会出问题:
// ❌ 错误:不能在 async 方法里直接 await 锁 await rwLock.EnterReadLockAsync(); // 编译不过!没有这个方法
正确做法只有两种:
在同步上下文中使用(如 ASP.NET Core 中标记[NonAction]或用
Task.Run(() => { ... }) 包裹锁内逻辑)
改用 AsyncReaderWriterLock(第三方 NuGet 包,如
Microsoft.VisualStudio.Threading提供的
AsyncReaderWriterLock) 更推荐:把 I/O 操作移出锁区,只锁内存结构修改,例如先读数据 → 解锁 → await DB 查询 → 再锁 → 更新缓存
真正难的不是写出来,而是判断该不该用写优先、要不要放弃
ReaderWriterLockSlim改用手动队列——这取决于你能否容忍写操作延迟超过 100ms。如果不能,就别碰默认模式。
