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

33
我正在使用一个并发字典作为线程安全的静态缓存,并注意到以下行为:
the MSDN docs on GetOrAdd:
如果您在不同的线程上同时调用GetOrAdd, addValueFactory可能会被多次调用,但其键/值对 可能不会添加到字典中的每个调用。
我希望能够保证工厂只被调用一次。是否有任何方法可以使用ConcurrentDictionary API来实现这一点,而不必采取自己的分离同步(例如,在valueFactory内部进行锁定)?
我的用例是valueFactory正在生成动态模块内的类型,因此如果两个相同密钥的valueFactories并发运行,则会出现System.ArgumentException:程序集内重复的类型名称。
2个回答

48
您可以使用这样的字典: ConcurrentDictionary<TKey, Lazy<TValue>>,然后您的值工厂将返回一个已初始化为LazyThreadSafetyMode.ExecutionAndPublicationLazy<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。以下是一些 扩展,可帮助解决此问题。

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
“LazyThreadSafetyMode.ExecutionAndPublication” 是默认值,可以省略。 - yaakov
1
@yaakov 确实。对我来说,默认值总是不太清楚,所以我喜欢始终明确指定它。 - usr
3
考虑到您正在扩展的方法数量,编写一个ConcurrentLazyDictionary类来透明地封装Lazy的使用可能更有意义。原意不变,语言通俗易懂。 - Ian Kemp

6
这在非阻塞算法中很常见。它们使用Interlock.CompareExchange来测试确认不存在争用的条件,但会循环直到CAS成功。可以查看ConcurrentQueue页面(4)作为非阻塞算法的良好入门。
简短回答是不需要,由于存在争用,将需要多次尝试向集合添加内容是其本质。除了使用传递值的其他重载之外,您需要保护您的值工厂免受多个调用的影响,可能需要使用双锁/内存屏障

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接