同步 ConcurrentDictionary
ConcurrentDictionary 的方法可以多次调用您的值工厂。 什么时候会出现问题? 怎么解决?
介绍
ConcurrentDictionary
在实现高性能同步方面解决了许多问题。 但它也有一个问题。它使用乐观的并发方式,其中值工厂可能会被执行多次。 让我们探索解决这个问题的方案...
典型用法
var dictionary = new ConcurrentDictionary<string, MyClass>();
Parallel.ForEach(source, dataitem =>{
var key = dataitem.Category;
var myObject = dictionary.GetOrAdd(key, k=>new MyClass());
/* Do some stuff with myObject */
});
上述示例是使用 ConcurrentDictionary
的一种可能应用。 基本上,您可能需要动态地用需要对其进行处理的对象自动填充字典。 这有点过于简单,但我只是想先概述一下这个模式。
ConcurrentDictionary
将生成一个 MyClass
对象(如果它在集合中不存在),并且 Parallel.ForEach
可能会同时请求相同的键。 如果碰巧条目不存在,并且两个线程请求相同的键,则 ConcurrentDictionary
将同时执行这两个构造函数并注册其中一个。 在构造函数很轻量且构造时间不长的情况下,这是一种非常高效的非阻塞同步方式。 在这种情况下,构造多个无关紧要,因为它仍然只向调用线程返回相同的已注册实例。 这非常有效,直到您生成一个 IDisposable
对象...
非典型情况...
那么,如果您的值工厂实际上做了一些重要的工作呢?
ConcurrentDictionary<object, object> c = /* Some static registry */;
object writeLock = null;
object lockEntry = null;
lockEntry = c.GetOrAdd(key, k =>
{
writeLock = new Object();
// First to get the lock owns it...
Monitor.Enter(writeLock);
return writeLock;
});
在此示例中,我们试图做的是允许对对象的第一个请求进行锁定,而任何后续请求都在排队等待。 如果 writeLock
不为 null
,那么您知道您拥有锁,如果为 null
,则您知道您没有锁。 这样做的问题是您最终可能会创建多个 writeLock,而实际上并没有得到您需要的。
快速修复
为了弥补创建多个对象并传递它的问题,需要在之后进行比较。 检查 writeLock 是否不为 null,然后检查它是否与 lockEntry 不匹配,这意味着它不是真正的锁对象。
if (writeLock != null && lockEntry != writeLock) { // This means that the concurrent dictionary executed GetOrAdd more than once (concurrently). // writeLock is then a local object that will not be accessed from other threads. // Therefore we do not own the lock exclusively.. Monitor.Exit(writeLock); // Is this object IDisposable? Call .Dispose() here. writeLock = null; }
如果将上述代码附加到之前的示例中,则问题已解决。 即使非阻塞 GetOrAdd
创建了两个写锁,您也可以确保只存在一个 writeLock
对象。
此模式还允许对最终被创建但未使用的 IDisposable
对象调用 Dispose()
。
如果我不能承受它发生多次怎么办?
如果您的值工厂运行时间很长并且您需要确保它只运行一次,该怎么办? 答案是 ConcurrentDictionary<TKey,Lazy<TValue>>
。
var dictionary = new ConcurrentDictionary<string, Lazy<MyClass>>();
Parallel.ForEach(source, dataitem =>{
var key = dataitem.Category;
var myObject = dictionary.GetOrAdd(key, k=>new Lazy<MyClass>(()=>{
/* Some potentially long running code */
},LazyThreadSafetyMode.ExecutionAndPublication)).Value;
/* Do some stuff with myObject */
});
结果是您只执行一次值工厂。 对于这种特定的用法,这非常高效,并且是一种最佳用法。 即使生成了多个 Lazy<MyClass>
对象,也只返回一个,并且 Value
属性在 Lazy<MyClass>
中是内部同步的。 您可能遇到的问题是,如果您需要按其值删除一个条目。 您将不得不逐个遍历集合,找到该对象的键,然后按键将其删除。
结论
就我个人而言,我喜欢 ConcurrentDictionary
并经常使用它。 但是偶尔,我必须处理这种类型的同步问题。 目前,.NET 框架中没有内置的 SynchronizedDictionary 类,可以确保仅执行一次。 但是这两种方法都利用了 ConcurrentDictionary
现有的非阻塞性能并解决了这个问题。