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

使用嵌套锁维护并发字典时是否存在死锁风险?

代码死锁风险分析与问题修复

首先明确:这段代码不存在死锁风险。死锁的核心条件是「循环等待」——线程A持有锁X并等待锁Y,线程B持有锁Y并等待锁X,但你的代码里所有线程获取锁的顺序完全一致:先拿_listLock全局锁,再拿对应UID的对象锁,不会出现循环等待的情况,所以死锁不会发生。

但这段代码有更致命的问题,远比重构死锁风险更紧急:

  • 全局锁彻底扼杀并发性能:_listLock是全局排他锁,所有调用RunWithLock的线程都必须排队等待这个锁,等于把所有操作串行化了。每分钟处理数千对象的高并发场景下,这会直接导致系统性能暴跌,完全发挥不出多核CPU的能力,比死锁的影响还严重。
  • 未初始化的_listLock会直接崩溃:_listLock没有赋值,第一次执行lock (_listLock)时会抛出NullReferenceException,这是必须立刻修复的bug。
  • 浪费了ConcurrentDictionary的线程安全特性:你用了线程安全的ConcurrentDictionary,却套了一层全局锁,完全没用到它的原子操作能力,属于冗余设计。

优化方案

去掉全局锁,利用ConcurrentDictionary的原子操作管理锁对象,同时给锁对象加引用计数,避免并发场景下锁对象被提前移除导致的线程安全问题:

private static readonly ConcurrentDictionary<Guid, (object LockObj, int RefCount)> _lockDict = new ConcurrentDictionary<Guid, (object, int)>();

public static void RunWithLock(Guid uid, Action body)
{
    // 原子创建或获取锁对象,并增加引用计数
    var entry = _lockDict.AddOrUpdate(
        uid,
        key => (new object(), 1),
        (key, existing) => (existing.LockObj, existing.RefCount + 1)
    );

    try
    {
        lock (entry.LockObj)
        {
            body();
        }
    }
    finally
    {
        // 原子减少引用计数,只有计数为0时才从字典移除
        while (true)
        {
            if (!_lockDict.TryGetValue(uid, out var current))
                break;

            var newRefCount = current.RefCount - 1;
            if (newRefCount == 0)
            {
                if (_lockDict.TryRemove(uid, out _))
                    break;
            }
            else
            {
                if (_lockDict.TryUpdate(uid, (current.LockObj, newRefCount), current))
                    break;
            }
        }
    }
}

这个优化后的版本:

  • 不同UID的操作可以并行执行,保留了高并发能力
  • 同一个UID的操作依然互斥,保证线程安全
  • 锁对象会在没有线程使用时被自动移除,保持字典整洁
  • 完全没有死锁风险

内容的提问来源于stack exchange,提问作者Pavel Grishin

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 06:20:27