【问题标题】:Why does ConcurrentDictionary.GetOrAdd(key, valueFactory) allow the valueFactory to be invoked twice?为什么 ConcurrentDictionary.GetOrAdd(key, valueFactory) 允许 valueFactory 被调用两次?
【发布时间】:2012-09-18 15:16:00
【问题描述】:

我正在使用并发字典作为线程安全的静态缓存,并注意到以下行为:

来自the MSDN docs on GetOrAdd

如果您在不同的线程上同时调用 GetOrAdd, addValueFactory 可能会被多次调用,但它的键/值对 可能不会为每次调用都添加到字典中。

我希望能够保证工厂只被调用一次。有什么方法可以使用 ConcurrentDictionary API 来做到这一点,而无需借助我自己的单独同步(例如锁定在 valueFactory 中)?

我的用例是 valueFactory 在动态模块中生成类型,所以如果同一键的两个 valueFactories 同时运行,我会点击:

System.ArgumentException: Duplicate type name within an assembly.

【问题讨论】:

    标签: c# multithreading caching concurrency concurrentdictionary


    【解决方案1】:

    您可以使用这样键入的字典:ConcurrentDictionary<TKey, Lazy<TValue>>,然后您的值工厂将返回一个已使用LazyThreadSafetyMode.ExecutionAndPublication 初始化的Lazy<TValue> 对象,这是Lazy<TValue> 使用的默认选项如果你不指定它。通过指定LazyThreadSafetyMode.ExecutionAndPublication,您告诉 Lazy 只有一个线程可以初始化并设置对象的值。

    这导致ConcurrentDictionary 仅使用Lazy<TValue> 对象的一个​​实例,而Lazy<TValue> 对象保护多个线程不对其值进行初始化。

    var dict = new ConcurrentDictionary<int, Lazy<Foo>>();
    dict.GetOrAdd(key,  
        (k) => new Lazy<Foo>(valueFactory)
    );
    

    缺点是每次访问字典中的对象时都需要调用 *.Value。这里有一些 extensions 会对此有所帮助。

    public static class ConcurrentDictionaryExtensions
    {
        public static TValue GetOrAdd<TKey, TValue>(
            this ConcurrentDictionary<TKey, Lazy<TValue>> @this,
            TKey key, Func<TKey, TValue> valueFactory
        )
        {
            return @this.GetOrAdd(key,
                (k) => new Lazy<TValue>(() => valueFactory(k))
            ).Value;
        }
    
        public static TValue AddOrUpdate<TKey, TValue>(
            this ConcurrentDictionary<TKey, Lazy<TValue>> @this,
            TKey key, Func<TKey, TValue> addValueFactory,
            Func<TKey, TValue, TValue> updateValueFactory
        )
        {
            return @this.AddOrUpdate(key,
                (k) => new Lazy<TValue>(() => addValueFactory(k)),
                (k, currentValue) => new Lazy<TValue>(
                    () => updateValueFactory(k, currentValue.Value)
                )
            ).Value;
        }
    
        public static bool TryGetValue<TKey, TValue>(
            this ConcurrentDictionary<TKey, Lazy<TValue>> @this,
            TKey key, out TValue value
        )
        {
            value = default(TValue);
    
            var result = @this.TryGetValue(key, out Lazy<TValue> v);
    
            if (result) value = v.Value;
    
            return result;
       }
    
       // this overload may not make sense to use when you want to avoid
       //  the construction of the value when it isn't needed
       public static bool TryAdd<TKey, TValue>(
           this ConcurrentDictionary<TKey, Lazy<TValue>> @this,
           TKey key, TValue value
       )
       {
           return @this.TryAdd(key, new Lazy<TValue>(() => value));
       }
    
       public static bool TryAdd<TKey, TValue>(
           this ConcurrentDictionary<TKey, Lazy<TValue>> @this,
           TKey key, Func<TKey, TValue> valueFactory
       )
       {
           return @this.TryAdd(key,
               new Lazy<TValue>(() => valueFactory(key))
           );
       }
    
       public static bool TryRemove<TKey, TValue>(
           this ConcurrentDictionary<TKey, Lazy<TValue>> @this,
           TKey key, out TValue value
       )
       {
           value = default(TValue);
    
           if (@this.TryRemove(key, out Lazy<TValue> v))
           {
               value = v.Value;
               return true;
           }
           return false;
       }
    
       public static bool TryUpdate<TKey, TValue>(
           this ConcurrentDictionary<TKey, Lazy<TValue>> @this,
           TKey key, Func<TKey, TValue, TValue> updateValueFactory
       )
       {
           if (!@this.TryGetValue(key, out Lazy<TValue> existingValue))
               return false;
    
           return @this.TryUpdate(key,
               new Lazy<TValue>(
                   () => updateValueFactory(key, existingValue.Value)
               ),
               existingValue
           );
       }
    }
    

    【讨论】:

    • LazyThreadSafetyMode.ExecutionAndPublication 是默认值,可以省略。
    • @yaakov 是的。对我来说,默认值总是不清楚,所以我喜欢总是指定它。
    • 考虑到您要扩展多少方法,编写一个透明封装Lazy 使用的ConcurrentLazyDictionary 类可能更有意义。
    【解决方案2】:

    这在Non-Blocking Algorithms 中并不少见。他们基本上使用Interlock.CompareExchange 测试确认没有争用的条件。他们循环直到CAS成功。看看 ConcurrentQueue 第 (4) 页作为对 Non-Blocking Algorithms 的一个很好的介绍

    简短的回答是否定的,这是野兽的本性,它需要多次尝试才能添加到争用的集合中。 除了使用传递值的其他重载之外,您还需要防止值工厂内的多次调用,可能使用double lock / memory barrier

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-11-26
      • 2015-11-27
      • 2021-07-13
      • 2021-11-25
      • 2015-12-24
      • 2012-12-10
      相关资源
      最近更新 更多