ReaderWriterLockSlim递归写锁错误及与Lock的差异咨询(异步场景)
ReaderWriterLockSlim 与常规 Lock 的核心区别
- 读写分离与并发效率:常规
lock是排他锁,不管读还是写,同一时间只能有一个线程持有锁;ReaderWriterLockSlim区分共享读锁和排他写锁——多个读线程可以同时持有读锁,只有写操作会独占锁,读多写少场景下能大幅提升并发能力。 - 递归锁默认禁用:
lock允许同一线程递归获取锁;ReaderWriterLockSlim默认不支持递归锁(除非构造时传入LockRecursionPolicy.SupportsRecursion),这就是你触发LockRecursionException的直接原因。 - 功能与性能:
ReaderWriterLockSlim比旧版ReaderWriterLock性能更优,还支持锁状态查询、读锁升级为写锁(或降级)等高级功能;lock在简单排他场景下更轻量,但读写混合场景不如前者灵活高效。
你的 async/await 场景问题
首先明确:ReaderWriterLockSlim 不兼容 async/await。它是基于线程绑定的锁,EnterWriteLock()后执行await会导致线程切换,最终释放锁的线程可能不是获取锁的线程,这会引发不可预料的问题;同时,await会让锁长时间持有,严重拖慢并发效率,甚至导致死锁。
你遇到的LockRecursionException,大概率是因为某个线程在持有写锁的情况下,又尝试再次获取写锁(比如addUrl方法内部也调用了returnRootDomainId),而默认配置下ReaderWriterLockSlim禁止递归获取写锁。
关于锁排队的疑问
ReaderWriterLockSlim的排队逻辑是:当写锁被持有时,所有后续的读锁、写锁请求都会进入等待队列。写锁释放后,默认会优先处理等待的写锁请求(可通过配置调整,但默认是写优先),再处理读锁请求。所以你说的A、B、C、D、E五个任务,当B持有写锁时,A、C、D、E都会等待;B释放锁后,队列里的任务会按顺序(写优先)逐个获取锁进入方法。
适合 async/await 的正确锁方案
因为你用了异步操作,必须使用异步感知的锁,推荐两种方案:
- SemaphoreSlim:支持异步等待(
WaitAsync()),可以作为排他锁使用,适配你的场景。 - AsyncReaderWriterLock:第三方库(如Nito.AsyncEx)提供的异步读写锁,支持读写分离的异步场景。
用SemaphoreSlim修改你的代码示例:
private static readonly SemaphoreSlim _lockRootAdd = new SemaphoreSlim(1, 1); private static async Task<int> returnRootDomainId(this string srUrl) { await _lockRootAdd.WaitAsync().ConfigureAwait(false); try { using ExampleCrawlerContext _context = new ExampleCrawlerContext(); string rootDomain = srUrl.NormalizeUrl().returnRootDomainUrl(); var rootDomainHash = rootDomain.SHA256Hash(); var result = await _context.RootDomains.Where(pr => pr.RootDomainUrlHash == rootDomainHash).FirstOrDefaultAsync().ConfigureAwait(false); if (result == null) { RootDomains _RootDomain = new RootDomains(); _RootDomain.RootDomainUrlHash = rootDomainHash; _context.Add(_RootDomain); await _context.SaveChangesAsync().ConfigureAwait(false); await addUrl(rootDomain).ConfigureAwait(false); } var result2 = await _context.RootDomains.Where(pr => pr.RootDomainUrlHash == rootDomainHash).FirstOrDefaultAsync().ConfigureAwait(false); return result2.RootDomainId; } finally { _lockRootAdd.Release(); } }
注意:如果addUrl内部也会调用returnRootDomainId,会触发递归等待导致死锁,建议重构逻辑避免递归调用,不要依赖递归锁解决问题。
内容的提问来源于stack exchange,提问作者Furkan Gözükara
相关产品推荐
相关产品推荐

