为什么ConcurrentDictionary.GetOrAdd(key,valueFactory)允许调用valueFactory两次?

时间:2012-09-26 22:04:26

标签: c# multithreading caching concurrency concurrentdictionary

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

来自the MSDN docs on GetOrAdd

  

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

我希望能够保证工厂只被召唤一次。是否有任何方法可以使用ConcurrentDictionary API执行此操作而无需借助我自己的单独同步(例如锁定valueFactory)?

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

System.ArgumentException: Duplicate type name within an assembly.

2 个答案:

答案 0 :(得分:38)

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

这导致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
       );
   }
}

答案 1 :(得分:5)

Non-Blocking Algorithms这种情况并不少见。他们基本上测试了使用Interlock.CompareExchange确认没有争用的情况。它们循环,直到CAS成功。请查看ConcurrentQueue第(4)页作为Non-Blocking Algorithms

的简介

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