你运气不错。由于测试可能会让你产生错误的安全感,所以这种线程错误很容易出现。事实证明,在有多个编写器时,
Dictionary<TKey, TValue>
不是线程安全的。文档明确说明:
“只要没有修改集合,
Dictionary<TKey, TValue>
即可支持多个读者并发。即使如此,枚举一个集合从本质上来说也不是线程安全的过程。在一个枚举与写访问相互竞争的罕见情况下,必须在整个枚举过程中锁定集合。为了允许多个线程读取和写入集合,必须实现自己的同步。”
或者,使用
ConcurrentDictionary
。但是,您仍然必须编写正确的代码(请参阅下面的注释)。
除了你现在幸运避免的
Dictionary<TKey, TValue>
的缺乏线程安全性外,你的代码存在严重缺陷。以下是你可能会遇到的代码bug:
static void IncreaseValue(int keyId, int adjustment) {
if (!KeyValueDictionary.ContainsKey(keyId)) {
KeyValueDictionary.Add(keyId, 0);
}
KeyValueDictionary[keyId] += adjustment;
}
- 字典为空。
- 线程1使用
keyId = 17
进入该方法。由于字典为空,if
中的条件返回 true
,线程1到达标有A
的代码行。
- 线程1暂停,线程2使用
keyId = 17
进入该方法。由于字典为空,if
中的条件返回 true
,线程2到达标有A
的代码行。
- 线程2暂停,线程1恢复。现在线程1将
(17, 0)
添加到字典中。
- 线程1暂停,现在线程2恢复。现在线程2尝试将
(17, 0)
添加到字典中。由于键冲突而抛出异常。
还有其他情况可能会导致异常发生。例如,当线程1正在加载 KeyValueDictionary[keyId]
的值(假设它加载了 keyId = 17
并获得了值 42
)时,线程2可能会进来并修改该值(假设它加载了 keyId = 17
,并添加了调整值27
),现在线程1恢复并将其调整值添加到加载的值中(特别是,它没有看到线程2对与 keyId = 17
关联的值所做的修改!)。
请注意,即使使用 ConcurrentDictionary<TKey, TValue>
也可能导致上述错误!您的代码存在与 Dictionary<TKey, TValue>
的线程安全或不安全无关的原因,而不是线程安全问题。
要使代码在使用并发字典时具有线程安全性,需要进行以下更改:
KeyValueDictionary.AddOrUpdate(keyId, adjustment, (key, value) => value + adjustment);
在这里,我们使用的是ConcurrentDictionary.AddOrUpdate
方法。