但是,到目前为止,在测试它时,从多个线程同时调用它时,我没有看到任何异常。
它是线程安全的还是到目前为止我很幸运?如果它是线程安全的,那为什么?
你走运了。这些类型的线程错误很容易产生,因为测试会给你一种错误的安全感,认为你做的事情是正确的。
事实证明,当您有多个编写者时,Dictionary<TKey, TValue> 不是线程安全的。文档明确指出:
只要不修改集合,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;
}
- 字典为空。
- 线程 1 进入带有
keyId = 17 的方法。由于 Dictionary 为空,if 中的条件返回true,线程1 到达标记为A 的代码行。
- 线程 1 暂停,线程 2 进入带有
keyId = 17 的方法。由于 Dictionary 为空,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。