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

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

来自GetOrAdd上的MSDN文档 :

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

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

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

System.ArgumentException: Duplicate type name within an assembly.

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

这导致ConcurrentDictionary仅使用Lazy对象的一个​​实例,而Lazy对象保护多个线程不会初始化其值。

 var dict = new ConcurrentDictionary>(); dict.GetOrAdd(key, (k) => new Lazy(valueFactory) ); 

不利的一面是,每次访问字典中的对象时,都需要调用* .Value。 以下是一些有助于此的扩展 。

 public static class ConcurrentDictionaryExtensions { public static TValue GetOrAdd( this ConcurrentDictionary> @this, TKey key, Func valueFactory ) { return @this.GetOrAdd(key, (k) => new Lazy(() => valueFactory(k)) ).Value; } public static TValue AddOrUpdate( this ConcurrentDictionary> @this, TKey key, Func addValueFactory, Func updateValueFactory ) { return @this.AddOrUpdate(key, (k) => new Lazy(() => addValueFactory(k)), (k, currentValue) => new Lazy( () => updateValueFactory(k, currentValue.Value) ) ).Value; } public static bool TryGetValue( this ConcurrentDictionary> @this, TKey key, out TValue value ) { value = default(TValue); var result = @this.TryGetValue(key, out Lazy 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( this ConcurrentDictionary> @this, TKey key, TValue value ) { return @this.TryAdd(key, new Lazy(() => value)); } public static bool TryAdd( this ConcurrentDictionary> @this, TKey key, Func valueFactory ) { return @this.TryAdd(key, new Lazy(() => valueFactory(key)) ); } public static bool TryRemove( this ConcurrentDictionary> @this, TKey key, out TValue value ) { value = default(TValue); if (@this.TryRemove(key, out Lazy v)) { value = v.Value; return true; } return false; } public static bool TryUpdate( this ConcurrentDictionary> @this, TKey key, Func updateValueFactory ) { if (!@this.TryGetValue(key, out Lazy existingValue)) return false; return @this.TryUpdate(key, new Lazy( () => updateValueFactory(key, existingValue.Value) ), existingValue ); } } 

这种情况对于非阻塞算法并不罕见。 他们基本上使用Interlock.CompareExchange测试确认没有争用的条件。 它们循环,直到CAS成功。 看看ConcurrentQueue页面(4)作为非阻塞算法的一个很好的介绍

简短的回答是不,这是野兽的本质,它需要多次尝试添加到争用的集合。 除了使用传递值的其他重载之外,您还需要防止值工厂内的多次调用,可能使用双锁/内存屏障 。