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等。

Dictionary<;中具有离散密钥的并发写入的线程安全性>;索引器

首先,我想注意的是,我在最近的项目中遇到了类似的情况,我们有一个关键字为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. }

这里似乎有多个多线程问题:

  1. freeListfreeCount字段不是volatile,因此对其的读/写可能容易出错
  2. 如果数十亿个线程此时此刻就在这里呢?:©
    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++;
    
  3. decrement操作不是线程安全的(@EricLippert给出了一个非常有趣的答案),所以我们可能会破坏字典的状态
  4. 如果数十亿个线程此时此刻就在这里呢?:©
    5. freeList = entries[index].next;
    一些线程将freeList值获取到index变量中,比如说我们在那里有5(条目中包含链表,head等于-1),并在重写freeList之前变为非活动状态。数十亿个线程一个接一个地推进链表,现在我们的第一个线程变为活动线程。他很高兴地用过时的数据覆盖freeList,在链表中创建了一个重影,那里的数据可以被覆盖

因此,在代码执行过程中可能会发生很多问题,我个人不建议您在这种情况下使用Dictionary类。