.NET - 字典锁定与ConcurrentDictionary

时间:2009-12-22 21:04:41

标签: .net concurrency dictionary concurrentdictionary

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

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

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

Reference to .NET 4.0's ConcurrentDictionary

8 个答案:

答案 0 :(得分:135)

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

考虑一家没有职员的商店,结账时除外。如果人们不采取负责任的行动,你会遇到很多问题。例如,假设客户从金字塔罐中取出罐头,而店员正在建造金字塔,所有地狱都会破裂。或者,如果两个客户同时到达同一个项目,谁赢了怎么办?会有斗争吗?这是一个非线程安全的集合。有很多方法可以避免问题,但它们都需要某种锁定,或者以某种方式明确访问。

另一方面,考虑在办公桌上有店员的商店,你只能通过他购物。你排队,问他一个项目,他把它带回给你,然后你走出了界限。如果你需要多件物品,你可以在每次往返时尽可能多地拾取物品,但是你需要小心避免占用店员,这会激怒你身后的其他顾客。

现在考虑一下。在有一个职员的商店里,如果你一路走到前面,然后问职员“你有没有卫生纸”,他说“是”,然后你去“好吧,我”当我知道我需要多少时,我会回复你,“那么当你回到前线时,商店当然可以卖光了。线程安全集合不会阻止这种情况。

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

非线程安全集合没有任何此类保证。例如,如果您在一个线程上向二叉树添加内容,而另一个线程忙于重新平衡树,则无法保证该项将被添加,或者即使该树在之后仍然有效,它可能已经超出了希望。

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

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

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

对于这种情况,您需要查看相关集合是否有安全的方法来获取您想要的内容,也许您需要重写上面的代码,或者您可能需要锁定。

答案 1 :(得分:58)

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

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

所以要使用它们,但不要认为它为你提供了一个免费的通行证来忽略所有的并发问题。你还需要小心。

答案 2 :(得分:34)

内部ConcurrentDictionary为每个哈希桶使用单独的锁。只要您只使用Add / TryGetValue等处理单个条目的方法,该字典就可以作为几乎无锁的数据结构,具有各自的甜蜜性能优势。 OTOH枚举方法(包括Count属性)一次锁定所有存储桶,因此在性能方面比同步字典差。

我会说,只需使用ConcurrentDictionary。

答案 3 :(得分:14)

你见过.Net 3.5sp1的Reactive Extensions吗?根据Jon Skeet的说法,他们已经为.Net3.5 sp1反向移植了一系列并行扩展和并发数据结构。

.Net 4 Beta 2有一组示例,详细描述了如何使用并行扩展。

我刚刚花了最后一周使用32个线程测试ConcurrentDictionary来执行I / O.它似乎像宣传的那样工作,这表明已经进行了大量的测试。

编辑:.NET 4 ConcurrentDictionary和模式。

微软发布了一个名为Patterns of Paralell Programming的pdf。它非常值得下载,因为它描述了非常好的细节,正确的模式用于.Net 4并发扩展和反模式,以避免。 Here it is.

答案 4 :(得分:13)

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

答案 5 :(得分:5)

基本上你想要使用新的ConcurrentDictionary。开箱即用,你必须编写更少的代码来制作线程安全的程序。

答案 6 :(得分:1)

我们已将ConcurrentDictionary用于缓存集合,每1小时重新填充一次,然后由多个客户端线程读取,to the solution问题类似Is this example thread safe?

我们发现,将其更改为ReadOnlyDictionary会改善整体效果。

答案 7 :(得分: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);

此代码是绝对安全的,但是使用ConcurrentDictionary的所有好处已荡然无存。

  1. 与使用普通Dictionary相比,性能变得更差,因为ConcurrentDictionary内部的内部锁定现在是浪费和多余的。
  2. 您必须检查所有代码以了解共享变量的未保护用途。
  3. 您难以使用笨拙的API(TryAdd ?, AddOrUpdate?)。

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