上一篇文章“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 的成本。