使用Dictionary<,>中的离散键进行并发写入的线程安全性索引

时间:2015-11-10 18:00:33

标签: c# .net multithreading dictionary concurrency

假设您有以下代码

var dictionary = new Dictionary<int, object>(capacity: 2500);

var uniqueKeys = Enumerable.Range(0, 1000).ToArray();

Parallel.ForEach(uniqueKeys, key => dictionary[key] = new object());

请注意,所有键都是唯一的,并且字典容量远远超过键的数量。

问题:是否存在导致此代码无法成功的条件?

鉴于current implementation of Dictionary<,>并且没有假设未来的假设内部变更,你能否展示任何不安全访问的证据?

备注:Thread safety with Dictionary<int,int> in .NetThread safety of a Dictionary<TKey, TValue>的副本,我不需要任何人告诉我ConcurrentDictionary等等。

1 个答案:

答案 0 :(得分:2)

首先,我想要注意的是,我在最近的项目中得到了类似的情况,我们有一个字典,密钥为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操作isn't thread-safe(来自@EricLippert的非常有趣的回答),所以我们的字典可能会被破坏。
  4. 如果此时数十亿的线程就在这里怎么办?:© 5. freeList = entries[index].next;
    某个线程将freeList值转换为index变量,比如说我们有5(条目包含其中的链接列表,头部等于-1)并变为非活动状态在覆盖freeList之前。数十亿个线程在位置后推进链表位置,现在我们的第一个变为活动状态。他高兴地用过时的数据覆盖freeList,在链表中创建鬼,并且可以覆盖数据。
  5. 因此在代码执行过程中可能会发生很多问题,而且我个人不建议您在这种情况下使用Dictionary类。