65.9K
CodeProject 正在变化。 阅读更多。
Home

同步 ConcurrentDictionary

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2013年2月14日

CPOL

3分钟阅读

viewsIcon

18392

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 现有的非阻塞性能并解决了这个问题。

© . All rights reserved.