线程安全的Dictionary.Add

19

当您只进行插入操作时,Dictionary.Add()是否是线程安全的?

我有一个在多个线程中插入键的代码,是否仍需要在Dictionary.Add()周围加锁?

在添加新键时,我遇到了此异常:

Exception Source:    mscorlib
Exception Type: System.IndexOutOfRangeException
Exception Message:   Index was outside the bounds of the array.
Exception Target Site: Insert
尽管很罕见, 我知道 Dictionary 不是线程安全的,尽管我认为仅仅调用 .Add 不会造成任何问题。
尽管很罕见, 我知道 Dictionary 不是线程安全的,尽管我认为仅仅调用 .Add 不会造成任何问题。
2个回答

28

无论是仅添加还是其他操作,字典(Dictionary)都不是线程安全的,因为它有一些内部结构需要保持同步,特别是当内部哈希桶得到重新调整大小时。

你要么在任何对其进行的操作周围实现自己的锁定,要么如果你在 .Net 4.0 中,则可以使用新的ConcurrentDictionary(绝对棒) - 它是完全线程安全的。

另一个选项 (更新)

话虽如此,还有另一种方法可以使用,但这将需要根据您插入到字典中的数据类型以及您的所有键是否保证唯一来进行一些微调:

给每个线程分配一个私有字典,然后插入其中。

当每个线程完成时,将所有字典合并成一个更大的字典; 如何处理重复的键取决于您。例如,如果您正在通过一个键缓存项目列表,那么您可以将每个具有相同键的列表合并为一个列表,并将其放置在主字典中。

关于性能的官方答案(在您接受后)

正如您的评论所说,您需要了解最佳方法(锁定或合并)以获得性能等。 我不能告诉您这将是什么; 最终需要进行基准测试。不过,我会尽力提供一些指导 :)

首先 - 如果您有任何关于Dictionar(y/ies)的最终条目数量的想法,请使用 (int) 构造函数来最小化调整大小。

合并操作可能是最佳的选择;因为没有线程会相互干扰。除非当两个对象共享相同的键时涉及的过程特别漫长;在这种情况下,强制将所有操作在操作的结尾放在单个线程上可能会抵消通过并行化第一阶段获得的所有性能增益!

同样,这里可能存在内存问题,因为您将有效地克隆字典,因此如果最终结果足够大,则可能会消耗大量资源;不过,它们将被释放。

如果需要在出现已存在的键时在线程级别做出决策,则需要使用 lock(){} 结构。

对于一个字典,这通常呈以下形式:

readonly object locker = new object();
Dictionary<string, IFoo> dictionary = new Dictionary<string, IFoo>();

void threadfunc()
{
  while(work_to_do)
  {
    //get the object outside the lock
    //be optimistic - expect to add; and handle the clash as a 
    //special case
    IFoo nextObj = GetNextObject(); //let's say that an IFoo has a .Name
    IFoo existing = null;
    lock(locker)
    {
      //TryGetValue is a god-send for this kind of stuff
      if(!dictionary.TryGetValue(nextObj.Name, out existing))
        dictionary[nextObject.Name] = nextObj;
      else
        MergeOperation(existing, nextObject);
    }
  }
}

如果MergeOperation非常慢,您可以考虑释放锁定,创建一个克隆对象,表示现有对象和新对象的合并,然后重新获取锁定。但是,您需要一种可靠的方法来检查在第一次锁定和第二次锁定之间现有对象的状态没有发生变化(版本号对此很有用)。


如果同时进行读写操作且读取次数超过写入次数,您可以查看ReaderWriterLockSlim;尽管从您的问题来看似乎不是这种情况。我已经在内存缓存中使用它,并且当插入操作足够复杂时,它比Monitor.Enter更好。 - Andras Zoltan
黄金法则(来自Donald Knuth):“我们应该忘记小的效率问题,大约有97%的时间:过早优化是万恶之源”。首先尝试一个直接的lock()语句(使用私有的new object()作为标记),看看效果如何。如果对性能满意,请坚持使用它 :) - Andras Zoltan
@Dr. Evil - 尽管您已经接受了,但我正在准备一些指导,以回应您的最后一条评论。这有点像瞎猜,但可能会提供一些帮助... - Andras Zoltan
@Dr. Evil - 实际上,我花了20分钟尝试想出一些简单的规则,但是我无法做到;如果不知道确切的流程,这太模糊了(最终我的答案增加了三倍,但只完成了一半!)抱歉! - Andras Zoltan
没问题,我认为我们仍然可以锁定它们,直到有一个好的理由不这样做为止。 - dr. evil
显示剩余5条评论

3

是的,当你在字典正在增加桶数时插入元素时,你可能会遇到这个异常。这是由另一个线程添加项目并且负载因子过高触发的。字典对此特别敏感,因为重新组织需要一段时间。好消息是,这使得你的代码很快崩溃,而不是每周只崩溃一次。

审查在线程中使用的每一行代码,并检查共享对象的使用位置。你还没有找到每周一次的崩溃。更糟糕的是,有些错误不会导致崩溃,只会偶尔生成错误数据。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接