什么时候应该使用ConcurrentDictionary和Dictionary?

93

我总是不知道该选择哪一个。按照我的理解,如果我想要两种数据类型作为KeyValue,以便我可以通过其key轻松找到一个值,那么我会使用 Dictionary 而非 List。但我总是困惑是否应该使用 ConcurrentDictionary 还是 Dictionary

在你责备我没有对此进行足够的研究之前,请听我说,我已经尝试过搜索,但似乎谷歌并没有关于 Dictionary vs. ConcurrentDictionary 的内容,而是关于每个单独的东西都有相关信息。

我以前问过一个朋友,但他们只是说:“如果你在代码中经常使用字典,请使用ConcurrentDictionary”,我并不想去纠缠他们进一步解释。有人能详细说明一下吗?


那么,在分别查看了这些对象的信息之后,为什么还是无法回答你何时应该使用它们中的每一个呢?如果你知道何时应该使用Dictionary和何时应该使用ConcurrentDictionary,鉴于你已经找到了这些信息,那么你就知道何时应该使用其中的一个而不是另一个。 - Servy
名称本身就解释了。当您需要并发访问字典时,您可以使用ConcurrentDictionary - Scott Chamberlain
3
如果要从多个线程访问字典,请使用ConcurrentDictionary。 整个System.Collections.Concurrent命名空间都是为此而设计的。 - Jonesopolis
要搜索的关键词是“线程安全”。 - Matthew Cawley
1
下面的答案有点不正确,因为它们提到了“线程”。Task也应该能够与ConcurrentDictionary一起使用。 - hyankov
显示剩余7条评论
5个回答

112
“如果您在代码中频繁使用字典,则使用ConcurrentDictionary是有点模糊的建议。我不怪你感到困惑。

ConcurrentDictionary主要用于在多个线程(或异步任务)更新字典的环境中使用。如果只来自单个线程,您可以从任何数量的代码中使用标准的Dictionary ;)

如果您查看ConcurrentDictionary上的方法,您会发现一些有趣的方法,例如TryAdd、TryGetValue、TryUpdate和TryRemove。

例如,考虑您可能会看到的与正常Dictionary类一起使用的典型模式。

// There are better ways to do this... but we need an example ;)
if (!dictionary.ContainsKey(id))
    dictionary.Add(id, value);

这里存在一个问题,即在检查是否包含某个键和调用Add之间,可能有不同的线程使用相同的id调用Add。当此线程调用Add时,它将引发异常。方法TryAdd会处理此类情况,并告诉您它是否已添加了该键(或该键是否已经存在于字典中)。

因此,除非您正在处理多线程代码段,否则您可以只使用标准的Dictionary类。话虽如此,您理论上可以使用锁来防止对字典的并发访问;这个问题已经在"Dictionary locking vs. ConcurrentDictionary"中得到了解答。


3
谢谢您的回答,它帮了很大的忙。即使来自单个线程,到处使用ConcurrentDictionary会有很大问题吗? - Ashkru
2
如果在创建线程之前进行编写(仅读取一个多线程),您可以使用非并发形式吗?我想这样会更快,并且无法看到它如何中断(除非您做了一些傻事,比如写入它)。 - ctrl-alt-delor
1
使用ConcurrentDictionary可能没有问题,但我想至少会有一些开销,这取决于字典的使用方式,可能会成为瓶颈。如果您想比较源代码以查看发生了什么,请查看ConcurrentDictionary.TryAddInternal()Dictionary.Insert()的源代码。 - Jeff B
我猜这个问题的主要目的是我不太明白什么情况下一个解决方案是多线程的,如果创建并启动了一个新线程,这是否特指它是多线程的,还是还有其他因素使应用程序成为多线程的?我看到他提到了异步任务,还有其他的吗? - Ashkru
1
@Konrad 这真的取决于你的具体代码以及字典是否从多个线程访问(以一种可能会产生竞争条件的方式),所以我能给出的最好答案是“也许” :P - Jeff B
显示剩余3条评论

11
使用 ConcurrentDictionary 而非普通的 Dictionary 的最大原因是线程安全。 如果您的应用程序将同时使用多个线程访问同一字典,则需要线程安全的 ConcurrentDictionary,特别是当这些线程正在写入或构建该字典时。
使用 ConcurrentDictionary 但没有多线程会带来开销。 所有使其具备线程安全性的函数仍将存在,所有锁定和检查都将发生,从而需要处理时间并使用额外的内存。

2
ConcurrentDictionary 在单线程代码中有什么缺点吗? - Aaron Franke
5
在上面的代码中,保证线程安全的功能仍然存在,所涉及的检查仍将进行,因此对象会更大且需要更多的处理才能使用。而且,阅读代码的其他人可能会寻找线程相关的内容,并感到困惑。 - Andrew

6

ConcurrentDictionary 在需要多线程(即多线程)访问字典时非常有用。普通的 Dictionary 对象不具备这种能力,因此应该只在单线程中使用。


如果即使是来自单个线程,到处都使用ConcurrentDictionary会是一件坏事吗? - Ashkru
1
不行,因为如果您不打算将程序设计成多线程,那么后来阅读您代码的人会感到困惑。此外,我预计任何并发数据结构都会有额外的开销,因为必须在多个线程之间管理访问权限。 - Woody1193
2
你说不行,然后就好像这是一件坏事似的,它真的是一件坏事吗?我问这个问题是因为目前它只能从主线程访问,但未来可能会发生变化。如果我决定实现多线程,是否值得使用 Concurrent 来处理所有情况或更改我的代码呢? - Ashkru
2
我不知道你的具体需求是什么。我只是展示使用它们的优缺点。如果你认为将来可能需要在多个线程中运行代码,并且你的字典可能会被跨越多个线程使用,那么使用ConcurrentDictionary将是一个有效的设计选择。然而,如果你没有计划这样做,那么你应该避免使用它,因为这种数据结构并不适合你的要求。 - Woody1193

5
一个 ConcurrentDictionary 在需要多个线程同时安全访问一个高性能字典时非常有用。相较于使用 lock 保护的标准 Dictionary,因为其粒度更小的锁定实现,它在重载情况下更加高效。 ConcurrentDictionary 内部维护了多个锁,而不是所有线程竞争一个单一锁,从而最大限度地减少争用并限制成为瓶颈的可能性。
尽管具有这些良好特性,但使用 ConcurrentDictionary 的最佳选项场景实际上非常有限,原因如下:
1. ConcurrentDictionary 提供的线程安全保证仅限于保护其内部状态。就是这样。如果想执行稍微复杂一点的操作(例如将字典和另一个变量更新为原子操作),就会遇到麻烦。这对于 ConcurrentDictionary 不是支持的方案。即使保护其包含的元素(如果它们是可变对象)也不受支持。如果尝试使用 AddOrUpdate 方法更新其中一个值,则字典将受到保护,但值不会受到保护。在此上下文中,“更新”意味着用另一个值替换现有值,而不是修改现有值。
2. 当你想要使用 ConcurrentDictionary 时,通常有更好的可用选择,这些选择不涉及共享状态,这正是 ConcurrentDictionary 的本质。无论其锁定方案多么有效,都很难击败一个根本没有共享状态、每个线程独立执行无干扰的架构。遵循此原则的常用库包括 PLINQTPL Dataflow 库。以下是 PLINQ 示例:
Dictionary<string, Product> dictionary = productIDs
    .AsParallel()
    .Select(id => GetProduct(id))
    .ToDictionary(product => product.Barcode);

不需要预先创建一个字典,然后由多个线程同时填充值。相反,您可以信任PLINQ使用更有效的策略来生成字典,包括对初始工作负载进行分区,并将每个分区分配给不同的工作线程。单个线程最终会聚合部分结果并填充字典。


4
上面的被接受的回答是正确的。然而,值得明确提出的是,如果一个字典没有被修改,即它只被读取,无论线程数量如何,那么首选Dictionary<TKey,TValue>,因为不需要进行同步。
例如,在应用程序的整个生命周期内,仅在启动时填充一次,并在整个应用程序中使用的缓存配置中使用Dictionary<TKey,TValue>何时使用线程安全集合:ConcurrentDictionary vs. Dictionary
如果您仅读取键或值,则Dictionary<TKey,TValue>更快,因为如果没有任何线程修改字典,则无需同步。

这是一个非常有用的答案!如果一个字典每分钟被读取100,000次,但每5分钟更新一次(使用锁),在这种情况下普通的字典仍然可以吗?还是每次读取都需要加锁? - Quade
@Quade 不幸的是,是的。请参见此处。如果在迭代过程中修改了字典,则它将变得不稳定。在这种情况下,请使用锁或考虑使用ConcurrentDictionary<TKey,TValue> - Grant Colley
感谢@GrantC。就性能而言,我认为字典会更快,显然锁定更新并不理想,只要可以非常快速地完成即可。 - Quade
@Quade,关于您每5分钟更新一次字典的问题,我有一个想法。在您的情况下,是否可以使用两个字典,一个用于读取,另一个用于更新?当更新完成时,在切换它们时取出锁定,即更新后的字典变为读取字典,而另一个则等待5分钟后再进行更新并切换回来。这样,唯一需要锁定的是在切换期间。 - Grant Colley
很棒的想法,似乎要快得多。 - Quade

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接