2012-09-26 28 views
26

我使用的是併發的字典作爲一個線程安全的靜態緩存,發現以下行爲:爲什麼ConcurrentDictionary.GetOrAdd(key,valueFactory)允許調用兩次valueFactory?

the MSDN docs on GetOrAdd

如果您在不同的線程同時調用GetOrAdd, addValueFactory可以被調用多次,但其鍵/值對 可能不會被添加到字典中的每個呼叫。

我希望能夠保證工廠只被調用一次。有沒有辦法用ConcurrentDictionary API來做到這一點,而不訴諸我自己的單獨同步(例如鎖定valueFactory內)?

我的使用情況是valueFactory正在生成類型的動態模塊內部,如果兩個valueFactories對於同一個密鑰同時運行我打:

System.ArgumentException:一個組件內有重複的類型名。

回答

35

你可以使用,它的類型像這樣的詞典:ConcurrentDictionary<TKey, Lazy<TValue>>,然後將你的價值工廠將返回已初始化LazyThreadSafetyMode.ExecutionAndPublication一個Lazy<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的每一個你在字典中訪問對象的時間。這裏有一些extensions,這將有助於這一點。

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 ([email protected](key, out Lazy<TValue> existingValue)) 
      return false; 

     return @this.TryUpdate(key, 
      new Lazy<TValue>(
       () => updateValueFactory(key, existingValue.Value) 
      ), 
      existingValue 
     ); 
    } 
} 
+0

'LazyThreadSafetyMode.ExecutionAndPublication'是默認值,可以忽略。 – yaakov

5

這並不罕見Non-Blocking Algorithms。他們基本上使用Interlock.CompareExchange測試確認沒有爭用的情況。它們循環,直到CAS成功。看看ConcurrentQueue頁面(4)作爲一個很好的介紹Non-Blocking Algorithms

簡短的回答是否定的,它是野獸的性質,它將需要多次嘗試添加到爭奪集合。 除了使用傳遞值的其他重載之外,您需要防止價值工廠內的多次調用,可能使用double lock/memory barrier

相關問題