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

f0r*_*0rt 23 .net c# parallel-processing multithreading

问题:我需要实现对象缓存.缓存需要是线程安全的,需要按需填充值(延迟加载).通过密钥(缓慢操作)通过Web服务检索值.所以我决定用ConcurrentDictionaryGetOrAdd() ,有一个值的工厂方法假定操作是原子操作和同步的方法.不幸的是,我在MSDN文章中找到了以下语句:如何:在ConcurrentDictionary中添加和删除项:

此外,尽管ConcurrentDictionary的所有方法都是线程安全的,但并非所有方法都是原子方法,特别是GetOrAdd和AddOrUpdate.传递给这些方法的用户委托是在字典的内部锁之外调用的.

那很不幸,但仍然没有完全回答我的回答.

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

Adi*_*ter 32

正如其他人已经指出的那样,valueFactory可能不止一次被调用.有一个通用的解决方案可以缓解这个问题 - 让您valueFactory返回一个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;
Run Code Online (Sandbox Code Playgroud)

Reed Copsey的博客文章进一步解释了这种方法

  • 这种方法不能保证工厂方法会被调用一次。事实是 Lazy&lt;T&gt; 是同步创建的,而且成本低廉,因此竞争条件更难发生,但它们仍然可能发生。 (10认同)
  • @Taudris 由“GetOrAdd”为给定键返回的“Lazy&lt;T&gt;”实例在所有“GetOrAdd”调用中都是相同的。因此竞争条件不会发生。为给定键创建新值的“GetOrAdd”并行调用可线性化,如下所示:创建值的第一个“GetOrAdd”调用将其存储在集合中。所有其他“GetOrAdd”调用都会创建它们的值并丢弃它们,返回已存储在集合中的值。 (3认同)
  • @BladeWise 怎么会这样?GetOrAdd 将始终返回集合中实际存在的值(因此始终是相同的值),然后 Lazy.Value 将仅调用工厂方法一次 (2认同)
  • 使用 `Lazy&lt;T&gt;` 而不是 `T` 只是“看起来”有效,因为创建 `Lazy&lt;T&gt;` 通常非常快。然而,这并不能 100% 保证正确的行为,因为相同的竞争条件仍然存在,并且不同的线程可能会获得不同的“Lazy&lt;T&gt;”实例。即使字典的内容一致,“GetOrAdd”返回的值也可能不一致。 (2认同)

Yuv*_*kov 20

是否每个键仅调用一次工厂值?

不,不是.文档说:

如果GetOrAdd同时在不同的线程上调用, 则可以多次调用valueFactory多次,但是对于每次调用,它的键/值对可能不会被添加到字典中.


Luc*_*ski 8

我们来看看源代码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;
}
Run Code Online (Sandbox Code Playgroud)

不幸的是,在这种情况下,valueFactory如果两个GetOrAdd调用碰巧并行运行,很明显没有任何保证不会被调用多次.