上一篇文章“ConcurrentDictionary 對決 Dictionary+Locking”中,我們知道了 .NET 4.0 中提供了線程安全的 ConcurrentDictionary<TKey, TValue> 類型,並在某些特定的使用條件下會產生問題。
在 ConcurrentDictionary<TKey, TValue> 類中有一個方法 GetOrAdd ,用於嘗試獲取一個鍵值,如果鍵值不存在則添加一個。其方法簽名如下:
public TValue GetOrAdd( TKey key, Func<TKey, TValue> valueFactory ) Parameters key Type: TKey The key of the element to add. valueFactory Type: System.Func<TKey, TValue> The function used to generate a value for the key
通常,我們會通過如下這種方式來使用:
ConcurrentDictionary<string, ExpensiveClass> dict1 = new ConcurrentDictionary<string, ExpensiveClass>(); string key1 = "111111"; ExpensiveClass value1 = dict1.GetOrAdd( key1, (k) => new ExpensiveClass(k));
這種使用方式會產生一個問題,就是如果特定的類的構造過程比較昂貴(資源消耗、時間消耗等),在並行運行條件下,當第一個線程嘗試獲取該鍵值時,發現不存在后開始構建該對象,而在構建的同時,另外一個線程也嘗試獲取該鍵值,發現不存在后也開始構建該對象,當第一個線程構造完畢后將對象添加至字典中,而第二個對象也構造完畢后會再次檢測字典中是否存在該鍵值,因為鍵值已經存在,所以將剛創建完畢的對象直接丟棄,而使用已存在的對象,這造成了對象構造過程中的浪費。如果是關注性能和資源的應用,此處就是一個需要改進的點。
我們假設這個類叫 ExpensiveClass 。
public class ExpensiveClass { public ExpensiveClass(string id) { Id = id; Console.WriteLine( "Id: [" + id + "] called expensive methods " + "which perhaps consume a lot of resources or time."); } public string Id { get; set; } }
類實例化的構造過程為什么昂貴可能有很多中情況,最簡單的例子可以為:
- 訪問了數據庫,讀取了數據,並緩存了數據。
- 訪問了遠程服務,讀取了數據,並緩存了數據。
- 將磁盤中的數據加載到內存中。
改進方式1:使用Proxy模式
我們可以使用 Proxy 模式來包裝它,通過 Proxy 中間的代理過程來隔離對對象的直接創建。
1 public class ExpensiveClassProxy 2 { 3 private string _expensiveClassId; 4 private ExpensiveClass _expensiveClass; 5 6 public ExpensiveClassProxy(string expensiveClassId) 7 { 8 _expensiveClassId = expensiveClassId; 9 } 10 11 public ExpensiveClass XXXMethod() 12 { 13 if (_expensiveClass == null) 14 { 15 lock (_expensiveClass) 16 { 17 if (_expensiveClass == null) 18 { 19 _expensiveClass = new ExpensiveClass(_expensiveClassId); 20 } 21 } 22 } 23 return _expensiveClass; 24 } 25 }
改進方式2:使用Lazy<T>模式
這種方式簡單易用,並且同樣解決了問題。
1 ConcurrentDictionary<string, Lazy<ExpensiveClass>> dict2 2 = new ConcurrentDictionary<string, Lazy<ExpensiveClass>>(); 3 4 string key2 = "222222"; 5 ExpensiveClass value2 = dict2.GetOrAdd( 6 key2, 7 (k) => new Lazy<ExpensiveClass>( 8 () => new ExpensiveClass(k))) 9 .Value;
在並行的條件下,同樣也存在構造了一個 Lazy<ExpensiveClass> 然后丟棄的現象,所以這種方式是建立在,構造 Lazy<T> 對象的成本要小於構造 ExpensiveClass 的成本。