c# 如何用c#实现读写者问题 Reader-Writer Problem

来源:这里教程网 时间:2026-02-21 17:40:11 作者:

什么是读写者问题的 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。如果不能,就别碰默认模式。

相关推荐