ConcurrentDictionary.GetOrAdd 多线程并发下的重复执行

ConcurrentDictionary 是 C# 中非常强大的线程安全集合之一,尤其是在多线程场景下表现出色。然而,它的 GetOrAdd(TKey key, Func<TKey, TValue> valueFactory) 方法在特定情况下会多次执行 valueFactory,这可能导致额外的性能消耗。本文将分析这个问题的根本原因,并提供一种结合 Lazy 的优雅解决方案。

  • 问题展示

    以下代码模拟了在项目中使用多个 Redis,不同的 Redis 通过 cluster_id 区分,并存放在 ConcurrentDictionary 中。理想情况下,每个 Redis 只实例化一个 RedisConnection 对象。

    internal class Program 
    { 
      static void Main(string[] args) 
      { 
          ConcurrentDictionary<string, RedisConnection> redisConnectionDic = new ConcurrentDictionary<string, RedisConnection>(); 
          string redisConn = "10"; 
          Parallel.For(110, i => 
          { 
              redisConnectionDic.GetOrAdd(redisConn, redisConn => { return CreateRedisClient(redisConn); }); 
          }); 
          Console.WriteLine("Hello, World!"); 
          Console.ReadLine(); 
      } 
     
      public static RedisConnection CreateRedisClient(string cluster_id) 
      { 
          Console.WriteLine($"正在初始化 cluster_id={cluster_id} 的redis 连接"); 
          Thread.Sleep(1000); 
          return new RedisConnection(); 
      } 
    } 
     
    public class RedisConnection 
    { } 
    

    运行结果分析

    运行上述代码后,控制台会输出多条“正在初始化 cluster_id=10 的redis 连接”,这表明CreateRedisClient被多次执行。

    原因剖析

    通过查看ConcurrentDictionary.GetOrAdd的源码,可以发现问题的根源:

    public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory) 
    { 
      if (key == null) 
      { 
          System.ThrowHelper.ThrowKeyNullException(); 
      } 
      if (valueFactory == null) 
      { 
          System.ThrowHelper.ThrowArgumentNullException("valueFactory"); 
      } 
      Tables tables = _tables; 
      IEqualityComparer<TKey> comparer = tables._comparer; 
      int hashCode = GetHashCode(comparer, key); 
      if (!TryGetValueInternal(tables, key, hashCode, out var value)) 
      { 
          TryAddInternal(tables, key, hashCode, valueFactory(key), updateIfExists: false, acquireLock: trueout value); 
      } 
      return value; 
    } 
     
    

    关键在于以下代码段:

    if (!TryGetValueInternal(tables, key, hashCode, out var value)) 
    { 
      TryAddInternal(tables, key, hashCode, valueFactory(key), updateIfExists: false, acquireLock: trueout value); 
    } 
    

    当第一个线程正在执行valueFactory(key)但尚未插入到 ConcurrentDictionary 时,其他线程可能会并发执行到此处,导致 valueFactory(key) 被多次调用。虽然最终只有一个结果会被存入 ConcurrentDictionary,但多次调用 valueFactory 是不可避免的。

    解决方案:结合 Lazy

    为了确保CreateRedisClient只执行一次,可以使用 Lazy 类来延迟初始化对象:

    internal class Program 
    { 
      static void Main(string[] args) 
      { 
          ConcurrentDictionary<string, Lazy<RedisConnection>> redisConnectionDic = new ConcurrentDictionary<string, Lazy<RedisConnection>>(); 
          string redisConn = "10"; 
          Parallel.For(110, i => 
          { 
              redisConnectionDic.GetOrAdd(redisConn, new Lazy<RedisConnection>(() => CreateRedisClient(redisConn))); 
              var connection = redisConnectionDic[redisConn].Value; 
          }); 
          Console.WriteLine("Hello, World!"); 
          Console.ReadLine(); 
      } 
     
      public static RedisConnection CreateRedisClient(string cluster_id) 
      { 
          Console.WriteLine($"正在初始化 cluster_id={cluster_id} 的redis 连接"); 
          Thread.Sleep(1000); 
          return new RedisConnection(); 
      } 
    } 
    public class RedisConnection 
    { } 
     
    

    Lazy 的作用

    Lazy<T> 是 .NET 中提供的一个线程安全的延迟初始化类,只有在访问Value属性时,才会执行初始化逻辑。结合Lazy后,即使多个线程并发调用GetOrAdd,初始化逻辑也只会执行一次。

    总结

  1. ConcurrentDictionaryGetOrAdd方法在多线程场景下无法保证valueFactory只执行一次,这可能带来性能问题。

  2. 通过引入Lazy,可以将初始化逻辑封装为线程安全的延迟加载,从而避免重复执行初始化代码。

posted @   dotNet编程拾光  阅读(29)  评论(0编辑  收藏  举报
编辑推荐:
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· 2 本地部署DeepSeek模型构建本地知识库+联网搜索详细步骤
点击右上角即可分享
微信分享提示