.Net 中的字典是不是可能在并行读取和写入时导致死锁?
Posted
技术标签:
【中文标题】.Net 中的字典是不是可能在并行读取和写入时导致死锁?【英文标题】:Is it possible for a Dictionary in .Net to cause dead lock when reading and writing to it in parallel?.Net 中的字典是否可能在并行读取和写入时导致死锁? 【发布时间】:2016-01-14 04:53:01 【问题描述】:我正在玩 TPL,并试图通过并行读取和写入同一个字典来找出我会造成多大的混乱。
所以我有这个代码:
private static void HowCouldARegularDicionaryDeadLock()
for (var i = 0; i < 20000; i++)
TryToReproduceProblem();
private static void TryToReproduceProblem()
try
var dictionary = new Dictionary<int, int>();
Enumerable.Range(0, 1000000)
.ToList()
.AsParallel()
.ForAll(n =>
if (!dictionary.ContainsKey(n))
dictionary[n] = n; //write
var readValue = dictionary[n]; //read
);
catch (AggregateException e)
e.Flatten()
.InnerExceptions.ToList()
.ForEach(i => Console.WriteLine(i.Message));
确实很乱,抛出了很多异常,主要是key不存在,还有一些是index out of array的异常。
但是应用运行一段时间后就挂了,cpu百分比停留在25%,机器有8核。 所以我假设这是 2 个线程满负荷运行。
然后我在上面运行了 dottrace,得到了这个:
这与我的猜测相符,两个线程以 100% 运行。
都运行 Dictionary 的 FindEntry 方法。
然后我再次运行应用程序,使用dottrace,这次结果略有不同:
这一次,一个线程运行 FindEntry,另一个线程运行 Insert。
我的第一个直觉是它是死锁的,但后来我认为不可能,只有一个共享资源,它没有被锁定。
那么这应该怎么解释呢?
ps:我不是要解决这个问题,它可以通过使用 ConcurrentDictionary 或并行聚合来解决。我只是在寻找一个合理的解释。
【问题讨论】:
你可以猜到 Findentry 试图找到一个条目。它保留了一些稍后更改的局部变量,这导致循环结束条件永远不会终止,因为它假定由另一个线程更改的条目数不会改变。 所以不是死锁,而是内部状态混乱导致的死循环? 【参考方案1】:所以你的代码正在执行Dictionary.FindEntry
。这不是死锁 - 当两个线程以某种方式阻塞时会发生死锁,这使得它们彼此等待释放资源,但在你的情况下,你会得到两个看似无限的循环。线程未锁定。
我们来看看reference source中的这个方法:
private int FindEntry(TKey key)
if( key == null)
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
if (buckets != null)
int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
for (int i = buckets[hashCode % buckets.Length]; i >= 0; i = entries[i].next)
if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) return i;
return -1;
看看for
循环。 increment 部分是i = entries[i].next
,猜猜看:entries
是在Resize
method 中更新的字段。 next
是内部Entry
struct的一个字段:
public int next; // Index of next entry, -1 if last
如果您的代码无法退出 FindEntry
方法,最可能的原因是您设法弄乱了条目,以至于当您遵循由next
字段。
至于Insert
method,它有一个非常相似的for
循环:
for (int i = buckets[targetBucket]; i >= 0; i = entries[i].next)
由于 Dictionary
类被证明是非线程安全的,因此无论如何您都处于未定义行为的领域。
使用ConcurrentDictionary
或诸如ReaderWriterLockSlim
之类的锁定模式(Dictionary
仅对并发读取是线程安全的)或普通的旧lock
可以很好地解决问题。
【讨论】:
如果一切都失败了,请阅读手册。如果这也失败了,请阅读源代码 -> 终极手册 @伟大的解释老兄! (+1) 这是一个了不起的解释。【参考方案2】:看起来像一个竞争条件(不是死锁) - 正如您所评论的那样,这会导致内部状态混乱。
字典不是线程安全的,因此从单独的线程(即使每个线程只有一个)并发读取和写入同一个容器是不安全的。
一旦达到竞争条件,将会发生什么变得不确定;在这种情况下,这似乎是某种无限循环。
一般来说,一旦需要写访问,就需要某种形式的同步。
【讨论】:
【参考方案3】:只是为了补充(和关联)前两个很好的答案:
Dictionary<T>
是一个 HashMap 实现,和大多数 HashMap 实现一样,它internally uses LinkedLists(用于存储多个元素,以防不同的键在经过散列和取散列模数后进入相同的桶位置)并使用内部数组它可以随着字典中元素数量的增长而增长。
由于代码以空字典开头并添加了很多元素,因此字典以small internal array(可能大小=3)开头并经常增加它。由于有多个线程尝试向字典中添加元素,不同线程很有可能同时尝试 Resize() 字典。由于 Dictionary 不是线程安全的类,如果两个线程同时尝试修改同一个 LinkedList,这可能会使 LinkedList 处于不一致状态(这是一个竞态条件 - 两个线程修改相同的数据,导致不可预知的结果)。
正如其他答案中所解释的,修改 LinkedList 时的竞争条件可能会使 LinkedList 进入无效状态,这将解释在迭代 LinkedList 的方法上发生的无限循环(FindEntry
和 Insert
) .这个无限循环解释了高 cpu(每个线程使用 100% cpu) - 如果它是死锁,则线程将处于低 cpu 状态,等待一些锁定的资源。
由于我们知道元素的数量,我们可以使用更大的大小(例如 1000000)预初始化字典,以减少出现竞争条件的机会。但这并不能解决问题 - 最好的解决方案仍然是使用线程安全类 (ConcurrentDictionary<T>
)。
【讨论】:
以上是关于.Net 中的字典是不是可能在并行读取和写入时导致死锁?的主要内容,如果未能解决你的问题,请参考以下文章