Dictionary<;中具有离散密钥的并发写入的线程安全性>;索引器
本文关键字:线程 gt 索引 并发 安全性 密钥 lt Dictionary | 更新日期: 2023-09-27 18:21:33
假设您有以下代码
var dictionary = new Dictionary<int, object>(capacity: 2500);
var uniqueKeys = Enumerable.Range(0, 1000).ToArray();
Parallel.ForEach(uniqueKeys, key => dictionary[key] = new object());
请注意,所有键都是唯一的,字典容量远远超过键的数量。
问题:是否有任何条件会导致此代码不成功?
考虑到Dictionary<,>
的当前实现,并且不假设未来假设的内部更改,您能出示任何不安全访问的证据吗?
备注:这是而不是.Net中Dictionary<int,int>
的线程安全或Dictionary<TKey, TValue>
的线程安全的副本,我不需要任何人告诉我ConcurrentDictionary
等。
首先,我想注意的是,我在最近的项目中遇到了类似的情况,我们有一个关键字为DateTimes
(唯一)的字典,并行处理它,在初始化它之后,我们有时会遇到KeyNotFoundException
的问题,但我们没有像您那样预分配内存。问题可能用它解决了吗?让我们谈谈您链接的代码。
每当我们得到关于并发性的问题时,我的多线程编程老师总是告诉我们同样的事情:
如果数十亿条线程此刻就在这里呢?
让我们试着看看Dictionary
中是否存在可能的问题dictionary[key] = new object()
引领我们走向
set {
Insert(key, value, false);
}
Insert
是主要的加法方法,在Dictionary
类中有很多地方被调用。当您声明对象是唯一的时,我假设那里不会有哈希冲突,也不会覆盖方法的第一个循环中的值,所以让我们看看代码的其余部分:
int index;
if (freeCount > 0) {
index = freeList;
freeList = entries[index].next;
freeCount--;
}
else {
if (count == entries.Length)
{
Resize();
targetBucket = hashCode % buckets.Length;
}
index = count;
count++;
}
当您用容量2500
初始化字典时,在这种情况下,else
子句似乎根本没有被调用,所以让我们来检查if
部分:
1. if (freeCount > 0) {
2. // atomic assign
3. index = freeList;
4. // some calculation and atomic assign
5. freeList = entries[index].next;
6. // not threadsafe operation
7. freeCount--;
8. }
这里似乎有多个多线程问题:
freeList
和freeCount
字段不是volatile
,因此对其的读/写可能容易出错如果数十亿个线程此时此刻就在这里呢?:©
3. index = freeList;
十亿个线程将获得相同的索引,因为freeList
字段的读写之间没有同步!之后,他们将用一个竞赛条件覆盖彼此的值:entries[index].hashCode = hashCode; entries[index].next = buckets[targetBucket]; entries[index].key = key; entries[index].value = value; buckets[targetBucket] = index; version++;
decrement
操作不是线程安全的(@EricLippert给出了一个非常有趣的答案),所以我们可能会破坏字典的状态- 如果数十亿个线程此时此刻就在这里呢?:©
5. freeList = entries[index].next;
一些线程将freeList
值获取到index
变量中,比如说我们在那里有5
(条目中包含链表,head等于-1
),并在重写freeList
之前变为非活动状态。数十亿个线程一个接一个地推进链表,现在我们的第一个线程变为活动线程。他很高兴地用过时的数据覆盖freeList
,在链表中创建了一个重影,那里的数据可以被覆盖
因此,在代码执行过程中可能会发生很多问题,我个人不建议您在这种情况下使用Dictionary
类。