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(1, 10, 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: true, out value); } return value; }关键在于以下代码段:
if (!TryGetValueInternal(tables, key, hashCode, out var value)) { TryAddInternal(tables, key, hashCode, valueFactory(key), updateIfExists: false, acquireLock: true, out 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(1, 10, 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,初始化逻辑也只会执行一次。总结
-
ConcurrentDictionary的GetOrAdd方法在多线程场景下无法保证valueFactory只执行一次,这可能带来性能问题。 -
通过引入
Lazy,可以将初始化逻辑封装为线程安全的延迟加载,从而避免重复执行初始化代码。

浙公网安备 33010602011771号