C#非恒定条件下的条件线程锁实现方案验证咨询
动态条件场景下的条件线程锁实现验证
核心需求
实现一套按需加锁的线程同步逻辑,满足:
- 线程安全方法A支持多线程高并发执行,无额外锁开销
- 非线程安全方法B执行时,完全阻塞A的执行,避免并发访问导致的数据错误
- 禁止使用全局全量加锁,满足缓存组件的极高性能要求
原有存在竞态的基础逻辑如下:
if (condition) { A(); } else { B(); } // 方法属性说明 // A:线程安全,基于并发字典实现缓存键更新计数累加 // B:非线程安全,负责处理更新记录、重排缓存优先级结构、淘汰最低优先级条目,原实现通过[MethodImpl(MethodImplOptions.Synchronized)]标记同步
其中判断条件condition为「请求的目标数据是否存在于缓存中」,B执行过程中会修改缓存结构、删除条目,可能导致正在执行A的线程访问已删除数据抛出错误。
已知的经典竞态场景
直接使用读写锁但未正确覆盖锁范围时,会出现如下问题:
线程1判断缓存命中(
condition=true),准备调用A前发生线程切换;线程2触发缓存未命中,执行B完成结构重排和条目淘汰,恰好删除了线程1要访问的缓存键;线程1恢复执行A时,访问已删除条目触发错误。
待验证的自定义实现
_lock.EnterReadLock(); if (condition) { A(); } _lock.ExitReadLock(); if (!condition) { B(); } void A() { // 缓存命中:并发字典累加更新计数 } void B() { _lock.EnterWriteLock(); // 缓存未命中:处理更新、重排结构、淘汰条目 _lock.ExitWriteLock(); }
方案有效性结论:无法解决竞态问题,存在两处致命缺陷
- 缺陷1:B的触发判断无锁保护,会导致重复执行
释放读锁后到B获取写锁前存在时间窗口,可能有多个线程同时读到condition=false,随后依次进入B获取写锁执行,重复触发结构重排、条目淘汰逻辑,既浪费性能,还可能因重复淘汰导致缓存数据错乱。 - 缺陷2:锁范围未覆盖空窗期,A/B并发问题依然存在
读锁释放后、B拿到写锁前,可能有新的线程成功获取读锁进入A执行。等B拿到写锁开始修改结构、删除条目时,这个正在运行的A依然会访问到已删除的条目,最初要规避的竞态问题完全没有被解决。
修正后的高性能实现方案
调整锁的覆盖范围,结合双重检查逻辑,既保证并发性能,又彻底消除竞态:
// 注意:生产环境优先使用ReaderWriterLockSlim,性能远优于旧版ReaderWriterLock private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); public void ProcessRequest(string key) { while (true) { // 快路径:缓存命中场景走读锁,支持多线程完全并发,无串行开销 _lock.EnterReadLock(); try { if (condition /* 缓存Key存在判断 */) { A(); return; } } finally { _lock.ExitReadLock(); } // 慢路径:缓存未命中时升级写锁,阻塞所有新的读请求 _lock.EnterWriteLock(); try { // 双重检查:等待写锁期间可能有其他线程已经完成了缓存加载,直接重试走快路径 if (condition) { continue; } B(); return; } finally { _lock.ExitWriteLock(); } } } void A() { // 缓存命中逻辑:并发字典累加更新计数 } void B() { // 缓存未命中逻辑:处理更新记录、重排优先级结构、淘汰最低优先级条目、加载新缓存条目 }
该方案的优势:
- 占比绝大多数的缓存命中场景全程只持有读锁,多线程无阻塞并发,完全满足高性能要求
- 只有缓存未命中的场景才会申请写锁,写锁持有期间所有新读请求会被阻塞,保证B执行时没有线程在运行A,彻底消除访问已删除条目的竞态
- 写锁入口的双重检查逻辑,避免了多线程重复执行B的重排、淘汰逻辑,减少无效性能损耗
注:ReaderWriterLockSlim是.NET官方推荐的高并发场景读写锁实现,相比旧版ReaderWriterLock没有线程亲和性带来的性能开销,递归锁检测机制也能规避大部分死锁风险,更适合缓存组件场景。
内容的提问来源于stack exchange,提问作者Kartik Mahajan
相关产品推荐
相关产品推荐

