【问题标题】:Is ConcurrentDictionary.GetOrAdd() guaranteed to invoke valueFactoryMethod only once per key?ConcurrentDictionary.GetOrAdd() 是否保证每个键只调用一次 valueFactoryMethod?
【发布时间】:2015-07-26 13:16:09
【问题描述】:

问题:我需要实现对象缓存。缓存需要是线程安全的,并且需要按需填充值(延迟加载)。这些值是通过 Web 服务通过 Key(慢速操作)检索的。所以我决定使用ConcurrentDictionary 和它的GetOrAdd() 方法,它有一个值工厂方法,假设操作是原子的和同步的。不幸的是,我在 MSDN 文章中找到了以下语句:How to: Add and Remove Items from a ConcurrentDictionary:

此外,虽然 ConcurrentDictionary 的所有方法都是 线程安全,并非所有方法都是原子的,特别是 GetOrAdd 和 添加或更新。传递给这些方法的用户委托是 在字典的内部锁之外调用。

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

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

【问题讨论】:

  • 不,使用 CD>。这是标准模式。

标签: c# .net multithreading parallel-processing


【解决方案1】:

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

具体来说:

// 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中有进一步的解释

【讨论】:

  • 对不起,我的错。我最初认为GetOrAdd 会返回由valueFactory 创建的值,因此两个GetOrAdd 调用可能会返回两个不同的结果(如链接帖子中的第一个评论者)。再想一想,这将是完全愚蠢的。此解决方案有效。
  • 这种方式不保证工厂方法会被调用一次。事实是 Lazy 是同步创建的,而且价格便宜,因此竞争条件更难发生,但它们仍然可能发生。
  • @BladeWise 怎么样? GetOrAdd 将始终返回集合中实际存在的值(因此始终是相同的值),然后 Lazy.Value 将只调用一次工厂方法
  • @Taudris GetOrAdd 为给定键返回的Lazy&lt;T&gt; 实例在所有GetOrAdd 调用中都是相同的。因此,竞态条件不会发生。为给定键创建新值的GetOrAdd 的并行调用可线性化如下:创建值的第一个GetOrAdd 调用将其存储在集合中。所有其他 GetOrAdd 调用创建它们的值并丢弃它们,返回已经存储在集合中的值。
  • 因此,即使多个线程可以最终创建 Lazy&lt;T&gt; 实例,也只会存储一个实例,并且所有线程都会返回该实例。看来你是对的;我会删除我的评论。
【解决方案2】:

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

不,不是。 The docs say:

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

【讨论】:

    【解决方案3】:

    我们来看看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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多