You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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 的正确锁方案

因为你用了异步操作,必须使用异步感知的锁,推荐两种方案:

  1. SemaphoreSlim:支持异步等待(WaitAsync()),可以作为排他锁使用,适配你的场景。
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 23:30:41