【问题标题】:Thread safety with Dictionary<int,int> in .Net.Net 中 Dictionary<int,int> 的线程安全
【发布时间】:2011-11-06 22:37:13
【问题描述】:

我有这个功能:

static Dictionary<int, int> KeyValueDictionary = new Dictionary<int, int>();
static void IncreaseValue(int keyId, int adjustment)
{
    if (!KeyValueDictionary.ContainsKey(keyId))
    {
        KeyValueDictionary.Add(keyId, 0);
    }
    KeyValueDictionary[keyId] += adjustment;
}

我原以为不会是线程安全的。但是,到目前为止,在测试它时,从多个线程同时调用它时,我没有看到任何异常。

我的问题:它是线程安全的还是到目前为止我很幸运?如果它是线程安全的,那为什么?

【问题讨论】:

  • 正如 Herb Sutter 所说(从记忆中):“当我们提交竞争条件时,竞争条件的典型行为是什么?它会通过单元测试和系统测试。我们有我们的发布派对。然后我们接到我们最重要的客户的电话,报告非确定性崩溃。”
  • @FuleSnabel:大声笑,很好。

标签: c# .net collections thread-safety


【解决方案1】:

但是,到目前为止,在测试它时,从多个线程同时调用它时,我没有看到任何异常。

它是线程安全的还是到目前为止我很幸运?如果它是线程安全的,那为什么?

你走运了。这些类型的线程错误很容易产生,因为测试会给你一种错误的安全感,认为你做的事情是正确的。

事实证明,当您有多个编写者时,Dictionary&lt;TKey, TValue&gt; 不是线程安全的。文档明确指出:

只要不修改集合,Dictionary&lt;TKey, TValue&gt; 可以同时支持多个读取器。即便如此,通过集合进行枚举本质上不是线程安全的过程。在枚举与写访问竞争的极少数情况下,必须在整个枚举期间锁定集合。 要允许集合被多个线程访问以进行读写,您必须实现自己的同步。

或者,使用ConcurrentDictionary。但是,您仍然必须编写正确的代码(参见下面的注释)。

除了您幸运地避免了Dictionary&lt;TKey, TValue&gt; 缺乏线程安全性之外,您的代码还存在危险的缺陷。以下是如何让您的代码出现错误:

static void IncreaseValue(int keyId, int adjustment) {
    if (!KeyValueDictionary.ContainsKey(keyId)) {
        // A
        KeyValueDictionary.Add(keyId, 0);
    }
    KeyValueDictionary[keyId] += adjustment;
}
  1. 字典为空。
  2. 线程 1 进入带有keyId = 17 的方法。由于 Dictionary 为空,if 中的条件返回true,线程1 到达标记为A 的代码行。
  3. 线程 1 暂停,线程 2 进入带有keyId = 17 的方法。由于 Dictionary 为空,if 中的条件返回true,线程2 到达标记为A 的代码行。
  4. 线程 2 暂停,线程 1 恢复。现在线程 1 将 (17, 0) 添加到字典中。
  5. 线程 1 暂停,现在线程 2 恢复。现在线程 2 尝试将 (17, 0) 添加到字典中。由于密钥冲突而引发异常。

还有其他可能发生异常的情况。例如,线程1可以在加载KeyValueDictionary[keyId]的值时暂停(比如加载keyId = 17,并获得42的值),线程2可以进来修改值(比如加载keyId = 17,添加调整27),现在线程1恢复并将其调整添加到它加载的值(特别是,它看不到线程2对与keyId = 17相关的值所做的修改!)。

请注意,即使使用ConcurrentDictionary&lt;TKey, TValue&gt; 也可能导致上述错误!由于与Dictionary&lt;TKey, TValue&gt; 的线程安全或缺乏线程安全无关的原因,您的代码不安全

要使用并发字典使您的代码成为线程安全的,您必须说:

KeyValueDictionary.AddOrUpdate(keyId, adjustment, (key, value) => value + adjustment);

这里我们使用ConcurrentDictionary.AddOrUpdate

【讨论】:

  • +1:很好的答案,感谢您的努力。特别喜欢你探索为什么“线程安全”集合仍然允许“线程不安全”代码的部分。
  • 感谢您花时间和精力整理这个答案。我最初的解决方案锁定了解决方案的全部内容,因为我不知道 ConcurrentDictionary 是一个更优雅的解决方案。
【解决方案2】:

.NET 库有一个线程安全字典,ConcurrentDictionary&lt;TKey, TValue&gt; http://msdn.microsoft.com/en-us/library/dd287191.aspx

更新:我没有完全回答这个问题,所以这里更新了对所提出的确切问题的更多回答。 根据 MSDN:http://msdn.microsoft.com/en-us/library/xfhwa508.aspx

一个字典可以同时支持多个阅读器, 只要集合没有被修改。即便如此,列举 通过集合本质上不是线程安全的过程。在 枚举与写访问竞争的罕见情况, 在整个枚举过程中必须锁定集合。为了让 集合被多个线程访问以进行读取和写入, 您必须实现自己的同步。

有关线程安全的替代方案,请参阅 ConcurrentDictionary。

这种类型的公共静态(在 Visual Basic 中为共享)成员是线程 安全。

【讨论】:

  • 这是危险的误导。即使将 OP 代码中的字典替换为 ConcurrentDictionary,代码仍然不安全。
【解决方案3】:

到目前为止,你很幸运。它不是线程安全的。

来自the Dictionary&lt;K,V&gt; documentation...

一个Dictionary&lt;TKey, TValue&gt;可以支持多个阅读器 同时,只要集合没有被修改。即使是这样, 通过集合枚举本质上不是线程安全的 程序。在枚举与写入竞争的极少数情况下 访问时,必须在整个枚举期间锁定集合。 允许集合被多个线程访问以进行读取 和写作,你必须实现自己的同步。

【讨论】:

    【解决方案4】:

    它不是线程安全的,但不会检查,因此可能不会注意到静默损坏。

    它在很长一段时间内看起来都是线程安全的,因为只有当它需要 rehash() 时它才有可能出现异常。否则,它只会损坏数据。

    【讨论】:

      猜你喜欢
      • 2015-10-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-12-09
      • 1970-01-01
      • 2020-07-22
      • 2011-01-03
      相关资源
      最近更新 更多