ConcurrentDictionary.GetOrAdd() 是否保证每个键只调用一次 valueFactoryMethod?

Is ConcurrentDictionary.GetOrAdd() guaranteed to invoke valueFactoryMethod only once per key?

问题:我需要实现对象缓存。缓存需要是线程安全的并且需要按需填充值(延迟加载)。这些值是通过 Key 的 Web 服务检索的(操作缓慢)。所以我决定使用 ConcurrentDictionary 及其 GetOrAdd() method that has a value factory method supposing that the operation is atomic and synchronized. Unfortunately I found the following statement in the MSDN article: How to: Add and Remove Items from a ConcurrentDictionary:

Also, although all methods of ConcurrentDictionary are thread-safe, not all methods are atomic, specifically GetOrAdd and AddOrUpdate. The user delegate that is passed to these methods is invoked outside of the dictionary's internal lock.

嗯,这很不幸,但仍然没有完全回答我的答案。

问题:每个键只调用一次值工厂吗?在我的具体情况下:是否有可能正在寻找相同密钥的多个线程针对相同的值对 Web 服务产生多个请求?

我们来看看source code of GetOrAdd:

public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory)
{
    if (key == null) throw new ArgumentNullException("key");
    if (valueFactory == null) throw new ArgumentNullException("valueFactory");

    TValue resultingValue;
    if (TryGetValue(key, out resultingValue))
    {
        return resultingValue;
    }
    TryAddInternal(key, valueFactory(key), false, true, out resultingValue);
    return resultingValue;
}

不幸的是,在这种情况下,如果两个 GetOrAdd 调用同时发生在 运行 上,显然没有任何东西可以保证 valueFactory 不会被调用多次。

Is value factory invoked only once per key?

不,不是。 The docs say:

If you call GetOrAdd simultaneously on different threads, valueFactory may be invoked multiple times, but its key/value pair might not be added to the dictionary for every call.

正如其他人已经指出的那样,valueFactory 可能会被多次调用。有一个缓解此问题的通用解决方案 - 让您的 valueFactory return 成为 Lazy<T> 实例。尽管可能会创建多个惰性实例,但实际的 T 值只会在您访问 Lazy<T>.Value 属性.

时创建

具体来说:

// Lazy instance may be created multiple times, but only one will actually be used.
// GetObjectFromRemoteServer will not be called here.
var lazyObject = dict.GetOrAdd("key", key => new Lazy<MyObject>(() => GetObjectFromRemoteServer()));

// Only here GetObjectFromRemoteServer() will be called.
// The next calls will not go to the server
var myObject = lazyObject.Value;

此方法在 Reed Copsey's blog post

中有进一步说明