ConcurrentDictionary键或值属性线程安全

Kod*_*oda 40 .net c# multithreading

有关于螺纹安全的问题ConcurrentDictionary.从API,我看到枚举器是线程安全的,但我没有看到键和值属性相同.我的问题是:

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

Ana*_*yal 56

虽然我喜欢这些文档,但我倾向于在有疑问的情况下用小程序验证一些东西,或者我觉得我可能会假设太多.

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

案例1:枚举值并删除键

如果您按照以下顺序:

  • 从线程开始枚举值集合
  • 从我们尚未枚举的其他线程中删除一个键
  • 继续枚举原始线程

观察到的行为是删除的键确实将被枚举,因为它在我们开始枚举时存在于值集合中.不会有例外.

案例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();
Run Code Online (Sandbox Code Playgroud)

以下是发布模式下输出:

控制台输出

  • 你真正说的是`Keys`和`Values`集合是你获得枚举器时字典的快照.几乎就像他们做了一个深刻的克隆并将它交还给你进行枚举. (12认同)
  • 希望Microsoft可以在MSDN中对此进行记录。 (2认同)
  • 此类实验的另一个更普遍的问题可能会导致真正错误的结论。如果某个特定框架实现在一个实验中以某种方式表现,如果文档没有明确“保证”,它并不意味着它在另一个框架版本中也会表现相同。 (2认同)

Hab*_*bib 16

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

资料来源:MSDN

  • +1,简洁!:)添加:是的,两个属性都用锁保护,你要枚举的是你开始枚举时字典**的内容**(所以你可能会看到一个键,例如,已经被从另一个线程删除). (4认同)

Tim*_*ith 9

是的,它的线程安全。但是,即使它是线程安全的,你应该不喜欢用KeysValues和也Count

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

查看参考源以了解原因 -Keys立即调用GetKeys()帮助程序并在继续之前获取每个锁。一旦它有了锁,它就会将每一个键复制到 a 中new List<TKey>,并返回一个只读视图——这样就不会有人意外地改变实际上只是键集合的临时副本的东西。这需要分配相当大的数组,并持有锁很长一段时间,如果你的集合变大了!

Values锁定和复制每个值类似于Keys. 甚至Count获取所有锁,不是为了复制,是为了求和所有内部表段的长度。所有这一切只是为了获得集合中对象的暂时“一致”计数,一旦锁定被释放,这仅作为粗略估计或历史脚注有用。

所以是的,叹息,如果你需要原子一致性,我想这可能是你必须付出的代价。但!也许你比那更幸运。然后,您可能会意识到您的场景实际上并不需要一致性,并且您可以掌握针对那些不良 API 的性能更高的 API - 例如使用它GetEnumerator()来大致了解您的集合中有哪些项目!GetEnumerator()的文档中的评论:

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

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