ConcurrentDictionary键或值属性线程安全

时间:2012-05-07 09:42:50

标签: c# .net multithreading

使用ConcurrentDictionary对线程安全性提出疑问。从API,我看到枚举器是线程安全的,但我没有看到键和值属性相同。我的问题是:

当有其他线程同时修改它时,循环遍历KeysValues集合是否安全?

3 个答案:

答案 0 :(得分:55)

虽然我喜欢这些文档,但是如果有疑问,我会倾向于使用小程序来验证,或者我觉得我可能会假设太多。

以下代码验证确实可以在将密钥从单独的线程添加或删除到枚举发生的位置时安全地枚举值集合。这不会导致通常的集合被修改异常。更详细地说,这里有几个测试用例

案例1:枚举值并删除密钥

如果您遵循以下顺序:

  • 开始枚举线程中的值集合
  • 从我们尚未枚举的其他主题中删除密钥
  • 继续枚举原始主题

观察到的行为是删除的密钥确实会被枚举,因为当我们开始枚举时它存在于values集合中。不会有例外。

案例2:枚举值并添加密钥

  • 开始枚举线程中的值集合
  • 从我们尚未枚举的其他线程添加新密钥
  • 继续枚举原始主题

观察到的行为是添加的密钥不会被枚举,因为当我们开始枚举它时,它不存在于值集合中。无论我们使用TryAdd还是直接分配到字典,即字典[key] = value,都不会引发异常。

示例代码

以下是演示这两种情况的示例程序:

ConcurrentDictionary<int, int> dictionary = new ConcurrentDictionary<int, int>();

// Seed the dictionary with some arbitrary values; 
for (int i = 0; i < 30; i++)
{
    dictionary.TryAdd(i, i);
}

// Reader thread - Enumerate the Values collection
Task.Factory.StartNew(
        () =>
        {
            foreach (var item in dictionary.Values)
            {
                Console.WriteLine("Item {0}: count: {1}", item, dictionary.Count);
                Thread.Sleep(20);
            }

        }
);

// writer thread - Modify dictionary by adding new items and removing existing ones from the end
Task.Factory.StartNew(
        () =>
        {
            for (int i = 29; i >= 0; i--)
            {
                Thread.Sleep(10);
                //Remove an existing entry 
                int removedValue;
                if (dictionary.TryRemove(i, out removedValue))
                    Console.WriteLine("Removed item {0}", removedValue);
                else
                    Console.WriteLine("Did not remove item {0}", i);

                int iVal = 50 + i*2;
                dictionary[iVal] = iVal;
                Thread.Sleep(10);
                iVal++;
                dictionary.TryAdd(iVal, iVal);
            }
        }
);

Console.ReadKey();

以下是发布模式中的输出

Console output

答案 1 :(得分:16)

  

ConcurrentDictionary表示键值的线程安全集合   可以由多个线程同时访问的对。

来源:MSDN

答案 2 :(得分:5)

是的,它的线程安全。 但是,即使它是线程安全的,您也应该不要使用KeysValuesCount

尤其是在使用ConcurrentCollections<T>时,因为要最大程度地减少锁争用,线程阻塞和内存分配。如果您关心性能和效率,则确实需要这些东西。

看看reference source来了解原因-Keys立即调用GetKeys()助手,并且获取每个锁,然后继续。拥有锁后,它将每个单键复制到new List<TKey>中,并返回一个只读视图-只是为了使某人不能意外地突变实际上只是该密钥的临时副本。密钥集合。如果您的集合很大,这需要分配相当大的数组,并持有锁一段时间。

Values类似于Keys锁定并复制每个值。甚至Count都获得所有锁,不是为了复制,而是为了汇总所有内部表段长度。仅仅是为了获得集合中对象的“一致”数量,这仅在释放锁定后用作粗略估计或历史脚注。

是的,叹气,如果您需要原子一致性,我想这可能是您必须付出的代价。但!也许您比这更幸运。然后,您可能会意识到您的方案实际上并不需要一致性,并且掌握了那些性能较差的API的更多优势就在您的掌握之中-就像使用GetEnumerator()来大致了解集合中的项目一样! GetEnumerator()的文档中的注释:

从字典返回的枚举数可以安全使用 与对字典的读取和写入同时进行,但是它确实 不代表字典的即时快照。的 通过枚举器公开的内容可能包含所做的修改 调用GetEnumerator之后转到字典。

换句话说,它根本不需要锁定或复制,因为它不需要确保一致性。哇!