45

有一个关于线程安全的问题ConcurrentDictionary。从 API 中,我看到枚举器是线程安全的,但对于键和值属性,我看不到相同的。我的问题是:

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

4

3 回答 3

60

虽然我确实喜欢文档,但当有疑问或我觉得我可能假设太多时,我倾向于用一个小程序来验证事情。

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

案例 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();

这是发布模式下的输出

控制台输出

于 2013-07-10T12:06:20.180 回答
17

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

资料来源:MSDN

于 2012-05-07T09:44:40.393 回答
9

是的,它的线程安全。但是,即使它是线程安全的,您也不应该使用Keys,Values和 also Count

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

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

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

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

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

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

于 2020-07-07T04:42:17.673 回答