.NET-字典锁定与ConcurrentDictionary


131

我找不到有关ConcurrentDictionary类型的足够信息,所以我想在这里问一下。

当前,我使用a Dictionary来保存由多个线程(从线程池,因此没有确切数量的线程)不断访问的所有用户,并且它具有同步访问。

我最近发现,.NET 4.0中有一组线程安全的集合,这看起来非常令人愉快。我想知道,什么是“更有效,更易于管理”的选项,因为我可以选择普通Dictionary访问与同步访问,或者选择已经ConcurrentDictionary是线程安全的访问。

对.NET 4.0的引用 ConcurrentDictionary


3
您可能希望看到有关此代码项目的文章
nawfal

Answers:


146

可以以不同的方式查看线程安全集合与非线程安全集合。

考虑一个没有店员的商店,除了结帐时。如果人们不负责任地采取行动,就会遇到很多问题。例如,假设某个客户从金字塔罐中拿出一个罐子,而店员正在建造金字塔时,地狱就会崩溃。或者,如果两个客户同时到达同一商品,谁会赢呢?会吵架吗?这是一个非线程安全的集合。有很多避免问题的方法,但是它们都需要某种锁定,或者以某种方式进行显式访问。

另一方面,考虑一个在桌子上有职员的商店,您只能通过他来购物。您排队,问他一个项目,他把它带回给您,您就不排队了。如果您需要多个物品,则每次往返只能拿起尽可能多的物品,但是您要小心避免挤弄业务员,这会激怒后面的其他顾客。

现在考虑一下。在商店里只有一个店员,如果您一直走到生产线的最前面,问店员“您有卫生纸吗”,然后他说“是”,然后您说“好,我是”当我知道我需要多少时,会尽快回覆给您”,然后当您回到生产线的最前面时,商店当然可以卖光了。线程安全集合不能阻止这种情况。

线程安全集合保证其内部数据结构始终有效,即使从多个线程进行访问也是如此。

非线程安全的集合不附带任何此类保证。例如,如果您在一个线程上向二进制树添加了某些内容,而另一个线程正忙于重新平衡该树,则无法保证将添加该项目,甚至无法保证此树在此之后仍然有效,那么它可能会损坏而无法预期。

但是,线程安全集合不能保证对该线程的顺序操作都可以在其内部数据结构的同一“快照”上进行,这意味着如果您具有以下代码:

if (tree.Count > 0)
    Debug.WriteLine(tree.First().ToString());

您可能会收到NullReferenceException,因为在tree.Count和之间tree.First(),另一个线程清除了树中的其余节点,这意味着First()将返回null

对于这种情况,您要么需要查看有问题的集合是否具有获取所需内容的安全方法,要么需要重写上面的代码,或者可能需要锁定。


9
每次我读这篇文章时,即使这都是真的,我们还是走了错误的道路。
scope_creep 2010年

19
启用线程的应用程序中的主要问题是可变数据结构。如果可以避免这种情况,则可以避免麻烦。
Lasse V. Karlsen

89
这是对线程安全性的很好解释,但是我不禁感到这实际上并没有解决OP的问题。OP提出的问题(以及我随后遇到的这个问题)是使用标准Dictionary和自己处理锁定与使用ConcurrentDictionary.NET 4+内置的类型之间的区别。我实际上对此感到困惑。
mclark1129 2013年

4
线程安全不仅仅是使用正确的集合。使用正确的集合是一个开始,而不是您唯一要做的事情。我想这就是OP想要知道的。我猜不出为什么接受了,自从我写这篇文章已经有一段时间了。
Lasse V. Karlsen 2013年

2
我认为@ LasseV.Karlsen想要说的是...线程安全很容易实现,但是您必须在如何安全方面提供一定程度的并发性。问问自己:“在保持对象线程安全的同时,我可以同时执行更多操作吗?” 考虑以下示例:两个客户想要退回不同的商品。当您作为经理出差去补货时,您是否不仅可以在一次出差中补货又可以同时满足两个客户的要求,还是每次客户退货时都要出差?
Alexandru

68

使用线程安全的集合时,您仍然需要非常小心,因为线程安全并不意味着您可以忽略所有线程问题。当一个集合将自己声明为线程安全的时,通常意味着即使多个线程同时进行读写,它也保持一致状态。但这并不意味着如果一个线程调用多个方法,它将看到“逻辑”结果序列。

例如,如果首先检查某个键是否存在,然后再获取与该键相对应的值,则即使使用ConcurrentDictionary版本,该键也可能不再存在(因为另一个线程可能已删除了该键)。在这种情况下,您仍然需要使用锁定(或者更好的方法是:使用TryGetValue组合两个调用)。

因此,请务必使用它们,但不要认为它给您免费通行证而忽略了所有并发问题。您仍然需要小心。


4
因此,基本上,您是说如果您没有正确使用该对象,它将无法正常工作?
2009年

3
基本上,您需要使用TryAdd而不是常用的Contains-> Add。
2009年

3
@Bruno:否。如果您尚未锁定,则另一个线程可能已删除了两次调用之间的键。
Mark Byers

13
并不是在这里听起来很刻薄,但TryGetValue它一直是Dictionary推荐的方法,并且一直是推荐的方法。ConcurrentDictionary引入的重要“并发”方法是AddOrUpdateGetOrAdd。因此,很好的答案,但可以选择一个更好的例子。
亚伦诺特,

4
正确-与具有事务处理的数据库不同,大多数并发的内存中搜索结构都缺少隔离的概念,它们仅保证内部数据结构正确。

39

内部ConcurrentDictionary为每个哈希存储区使用单独的锁。只要您仅使用Add / TryGetValue以及适用于单个条目的类似方法,该词典就可以用作几乎无锁的数据结构,并具有各自的出色性能。OTOH枚举方法(包括Count属性)一次锁定所有存储桶,因此,从性能角度来看,它比同步的Dictionary更糟糕。

我会说,只需使用ConcurrentDictionary。


15

我认为ConcurrentDictionary.GetOrAdd方法正是大多数多线程方案所需要的。


1
我也将使用TryGet,否则,每当密钥已经存在时,您就将精力浪费在初始化第二个参数GetOrAdd上。
Jorrit Schippers 2013年

7
GetOrAdd具有重载GetOrAdd(TKey,Func <TKey,TValue>),因此只有在字典中不存在该键的情况下,才能对第二个参数进行延迟初始化。此外,TryGetValue用于获取数据,而不进行修改。对这些方法中的每个方法的后续调用可能导致另一个线程可能在两个调用之间添加或删除了键。
sgnsajgon 2014年

即使Lasse的帖子很棒,这也应该是该问题的明显答案。
Spivonious

13

您是否看到过.NET 3.5sp1 的Reactive Extensions。根据Jon Skeet的说法,他们已经向后移植了.Net3.5 sp1的并行扩展和并发数据结构包。

.Net 4 Beta 2有一组示例,其中非常详细地介绍了如何使用它们的并行扩展。

我上周刚用32个线程执行I / O测试了ConcurrentDictionary。它似乎像广告中所说的那样工作,这表明已经进行了大量测试。

编辑:.NET 4 ConcurrentDictionary和模式。

微软已经发布了名为Paralell编程模式的pdf文件。正如非常详细的描述中所述,它确实值得下载,它适用于.Net 4并发扩展的正确模式和避免的反模式。这里是。


4

基本上,您想使用新的ConcurrentDictionary。开箱即用,您只需编写更少的代码即可创建线程安全的程序。


如果我继续进行并发词典有什么问题吗?
kbvishnu 2012年

1

ConcurrentDictionary是一个很好的选择,如果它符合所有的线程安全的需求。如果不是这样,换句话说,您正在做的事情有些复杂,那么正常Dictionary+ lock可能是更好的选择。例如,假设您要向词典中添加一些订单,而您想保持更新的订单总数。您可以编写如下代码:

private ConcurrentDictionary<int, Order> _dict;
private Decimal _totalAmount = 0;

while (await enumerator.MoveNextAsync())
{
    Order order = enumerator.Current;
    _dict.TryAdd(order.Id, order);
    _totalAmount += order.Amount;
}

此代码不是线程安全的。多个线程更新该_totalAmount字段可能会使它处于损坏状态。因此,您可以尝试使用来保护它lock

_dict.TryAdd(order.Id, order);
lock (_locker) _totalAmount += order.Amount;

该代码“更安全”,但仍然不是线程安全的。不能保证_totalAmount与字典中的条目一致。另一个线程可能尝试读取这些值以更新UI元素:

Decimal totalAmount;
lock (_locker) totalAmount = _totalAmount;
UpdateUI(_dict.Count, totalAmount);

totalAmount未必对应的订单在字典中的计数。显示的统计信息可能是错误的。在这一点上,您将意识到您必须扩展lock保护以包括字典的更新:

// Thread A
lock (_locker)
{
    _dict.TryAdd(order.Id, order);
    _totalAmount += order.Amount;
}

// Thread B
int ordersCount;
Decimal totalAmount;
lock (_locker)
{
    ordersCount = _dict.Count;
    totalAmount = _totalAmount;
}
UpdateUI(ordersCount, totalAmount);

这段代码是完全安全的,但是使用a的所有好处ConcurrentDictionary都已荡然无存。

  1. 性能已经比使用normal变得更差了Dictionary,因为内部的内部锁定ConcurrentDictionary现在是浪费和冗余的。
  2. 您必须检查所有代码以了解共享变量的未保护用途。
  3. 您在使用笨拙的API(TryAdd?,AddOrUpdate?)时会遇到麻烦。

因此,我的建议是:从Dictionary+ 开始lock,并保留稍后升级到a的选项,以ConcurrentDictionary优化性能(如果该选项实际上可行)。在许多情况下,事实并非如此。


0

我们使用ConcurrentDictionary的缓存集合,也就是再填充每隔1小时,然后通过多个客户端线程读取,类似的解决方案是这个示例线程安全吗?题。

我们发现,将其更改为  ReadOnlyDictionary可以  提高整体性能。


ReadOnlyDictionary只是另一个IDictionary的包装。您在引擎盖下使用什么?
Lu55

@ Lu55,我们使用的是MS .Net库,并在可用/适用的词典中进行选择。我不知道ReadOnlyDictionary的MS内部实现
Michael Freidgeim '18
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.