.NET - 字典锁定与 ConcurrentDictionary
Posted
技术标签:
【中文标题】.NET - 字典锁定与 ConcurrentDictionary【英文标题】:.NET - Dictionary locking vs. ConcurrentDictionary 【发布时间】:2010-12-29 06:42:56 【问题描述】:我找不到关于 ConcurrentDictionary
类型的足够信息,所以我想在这里问一下。
目前,我使用Dictionary
来保存由多个线程(来自线程池,因此没有确切数量的线程)不断访问的所有用户,并且它具有同步访问。
我最近发现.NET 4.0中有一组线程安全的集合,看起来很讨人喜欢。我想知道,“更高效、更易于管理”的选项是什么,因为我可以选择使用具有同步访问的普通 Dictionary
,或者拥有已经是线程安全的 ConcurrentDictionary
。
Reference to .NET 4.0's ConcurrentDictionary
【问题讨论】:
你可能想看看this code project article on the subject 【参考方案1】:基本上你想使用新的 ConcurrentDictionary。开箱即用,您必须编写更少的代码来制作线程安全的程序。
【讨论】:
如果我继续使用并发字典有什么问题吗?【参考方案2】:在使用线程安全集合时仍然需要非常小心,因为线程安全并不意味着您可以忽略所有线程问题。当一个集合宣称自己是线程安全的时,通常意味着即使多个线程同时读取和写入,它也会保持一致的状态。但这并不意味着如果单个线程调用多个方法,它就会看到“逻辑”的结果序列。
例如,如果您首先检查一个键是否存在,然后获取与该键对应的值,那么即使使用 ConcurrentDictionary 版本,该键也可能不再存在(因为另一个线程可能已经删除了该键)。在这种情况下,您仍然需要使用锁定(或者更好:使用 TryGetValue 组合两个调用)。
所以请务必使用它们,但不要认为它可以让您免费忽略所有并发问题。你还是要小心。
【讨论】:
所以基本上你是说如果你没有正确使用这个对象就不会工作? 基本上你需要使用 TryAdd 而不是常见的 Contains -> Add。 @Bruno:不。如果你没有锁定,可能是另一个线程删除了两次调用之间的密钥。 这里并不是说听起来很生硬,但TryGetValue
一直是Dictionary
的一部分,并且一直是推荐的方法。 ConcurrentDictionary
引入的重要“并发”方法是AddOrUpdate
和GetOrAdd
。所以,很好的答案,但可以选择一个更好的例子。
对 - 与有事务的数据库不同,大多数并发的内存搜索结构都缺少隔离的概念,它们只保证内部数据结构是正确的。【参考方案3】:
我认为 ConcurrentDictionary.GetOrAdd 方法正是大多数多线程场景所需要的。
【讨论】:
我也会使用 TryGet,否则每次密钥已经存在时,您都会浪费精力将第二个参数初始化为 GetOrAdd。 GetOrAdd 有重载 GetOrAdd(TKey, Func可以以不同的方式看待线程安全集合与非线程安全集合。
考虑一个没有店员的商店,结账时除外。如果人们不负责任地行事,就会有很多问题。例如,假设客户从金字塔罐中取出罐头,而店员目前正在建造金字塔,所有地狱都会崩溃。或者,如果两个顾客同时拿到同一个商品,谁会赢?会打架吗?这是一个非线程安全的集合。有很多方法可以避免问题,但它们都需要某种锁定,或者以某种方式显式访问。
另一方面,考虑一个在办公桌前有店员的商店,您只能通过他购物。你排队,向他要一件东西,他把它拿回来给你,你就离开了。如果你需要多件物品,每次往返只能取你能记住的尽可能多的物品,但你要小心,不要挤到店员身上,这会激怒你后面排队的其他顾客。
现在考虑一下。在有一个店员的商店里,如果你一直排到最前面,问店员“你有卫生纸吗”,他说“有”,然后你去“好吧,我”当我知道我需要多少时会回复你”,然后当你回到队伍的最前面时,商店当然可以卖光了。线程安全集合不会阻止这种情况。
线程安全集合保证其内部数据结构始终有效,即使从多个线程访问也是如此。
非线程安全的集合没有任何此类保证。例如,如果您在一个线程上向二叉树添加一些内容,而另一个线程正忙于重新平衡树,则无法保证该项目将被添加,或者即使该树在之后仍然有效,它可能已损坏到无法预料的程度。
但是,线程安全集合并不能保证线程上的顺序操作都在其内部数据结构的同一个“快照”上工作,这意味着如果您有这样的代码:
if (tree.Count > 0)
Debug.WriteLine(tree.First().ToString());
您可能会收到 NullReferenceException,因为在 tree.Count
和 tree.First()
之间,另一个线程已清除树中的剩余节点,这意味着 First()
将返回 null
。
对于这种情况,您要么需要查看有问题的集合是否有安全的方式来获取您想要的东西,也许您需要重写上面的代码,或者您可能需要锁定。
【讨论】:
每次我读到这篇文章,尽管这都是真的,但不知何故我们走错了路。 启用线程的应用程序的主要问题是可变数据结构。如果你能避免这种情况,你就会为自己省去很多麻烦。 这是对线程安全的一个很好的解释,但是我不禁觉得这实际上并没有解决 OP 的问题。 OP 所问的(以及我随后遇到的这个问题)是使用标准Dictionary
和自己处理锁定与使用 .NET 4+ 中内置的 ConcurrentDictionary
类型之间的区别。我实际上有点困惑,这被接受了。
线程安全不仅仅是使用正确的集合。使用正确的集合是一个开始,而不是您必须处理的唯一事情。我想这就是OP想知道的。我猜不出为什么这被接受了,我写这篇文章已经有一段时间了。
我认为@LasseV.Karlsen 想说的是……线程安全很容易实现,但是您必须在处理该安全性时提供一定程度的并发性。问问自己,“我可以在保持对象线程安全的同时进行更多操作吗?”考虑这个例子:两个客户想要退回不同的物品。当您作为经理前往为您的商店补货时,您是否可以不只是一次补货两件商品并仍然处理两个客户的请求,还是每次客户退货时您都打算去一趟? 【参考方案5】:
您是否看过 .Net 3.5sp1 的 Reactive Extensions。根据 Jon Skeet 的说法,他们已经为 .Net3.5 sp1 反向移植了一组并行扩展和并发数据结构。
.Net 4 Beta 2 有一组示例,其中非常详细地描述了如何在并行扩展中使用它们。
上周我刚刚使用 32 个线程来测试 ConcurrentDictionary 以执行 I/O。它似乎像宣传的那样工作,这表明已经进行了大量的测试。
编辑:.NET 4 ConcurrentDictionary 和模式。
Microsoft 发布了一个名为“并行编程模式”的 pdf。它非常值得下载,因为它非常详细地描述了用于 .Net 4 并发扩展的正确模式以及要避免的反模式。 Here it is.
【讨论】:
【参考方案6】:内部 ConcurrentDictionary 为每个哈希桶使用单独的锁。只要你只使用 Add/TryGetValue 和类似的方法来处理单个条目,字典将作为一个几乎无锁的数据结构工作,并具有各自的良好性能优势。 OTOH 枚举方法(包括 Count 属性)一次锁定所有存储桶,因此在性能方面比同步字典更差。
我会说,只需使用 ConcurrentDictionary。
【讨论】:
【参考方案7】:我们已将 ConcurrentDictionary 用于缓存集合,每 1 小时重新填充一次,然后由多个客户端线程读取,类似于 Is this example thread safe? 问题的 to the solution。
我们发现,将其更改为 ReadOnlyDictionary 会提高整体性能。
【讨论】:
ReadOnlyDictionary 只是另一个 IDictionary 的包装。您在引擎盖下使用什么? @Lu55,我们使用 MS .Net 库并在可用/适用的字典中进行选择。我不知道 ReadOnlyDictionary 的 MS 内部实现 Link only answer,现在那些页面已经消失了。谢谢。 @Dementic,哪个链接坏了?我可以打开所有 3 个链接以上是关于.NET - 字典锁定与 ConcurrentDictionary的主要内容,如果未能解决你的问题,请参考以下文章
csharp ReaderWriterLockByKey - 允许通过键将线程安全和读/写/升级锁定应用于任何类字典对象。在内部使用它