使用.Net中的Dictionary <int,int>进行线程安全

Guy*_*Guy 9 .net c# collections thread-safety

我有这个功能:

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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

jas*_*son 22

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

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

你很幸运.这些带有线程的bug很容易制作,因为测试可以给你一种错误的安全感,你可以正确地做事.

事实证明,Dictionary<TKey, TValue>当你有多个编写器时,它不是线程安全的.文档明确指出:

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

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

除了缺乏线程安全性之外Dictionary<TKey, TValue>,您有幸避免使用它,您的代码存在危险的缺陷.以下是您如何获得代码错误:

static void IncreaseValue(int keyId, int adjustment) {
    if (!KeyValueDictionary.ContainsKey(keyId)) {
        // A
        KeyValueDictionary.Add(keyId, 0);
    }
    KeyValueDictionary[keyId] += adjustment;
}
Run Code Online (Sandbox Code Playgroud)
  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!).

请注意,即使使用a ConcurrentDictionary<TKey, TValue>也可能导致上述错误!您的代码不安全,原因与线程安全性无关或缺乏线程安全性Dictionary<TKey, TValue>.

要使用并发字典使代码具有线程安全性,您必须说:

KeyValueDictionary.AddOrUpdate(keyId, adjustment, (key, value) => value + adjustment);
Run Code Online (Sandbox Code Playgroud)

我们在这里使用ConcurrentDictionary.AddOrUpdate.

  • +1:很好的答案,谢谢你的努力.特别喜欢你探索为什么"线程安全"集合仍然允许"线程不安全"代码. (5认同)