我找不到有关ConcurrentDictionary
类型的足够信息,所以我想在这里问一下。
当前,我使用a Dictionary
来保存由多个线程(从线程池,因此没有确切数量的线程)不断访问的所有用户,并且它具有同步访问。
我最近发现,.NET 4.0中有一组线程安全的集合,这看起来非常令人愉快。我想知道,什么是“更有效,更易于管理”的选项,因为我可以选择普通Dictionary
访问与同步访问,或者选择已经ConcurrentDictionary
是线程安全的访问。
我找不到有关ConcurrentDictionary
类型的足够信息,所以我想在这里问一下。
当前,我使用a Dictionary
来保存由多个线程(从线程池,因此没有确切数量的线程)不断访问的所有用户,并且它具有同步访问。
我最近发现,.NET 4.0中有一组线程安全的集合,这看起来非常令人愉快。我想知道,什么是“更有效,更易于管理”的选项,因为我可以选择普通Dictionary
访问与同步访问,或者选择已经ConcurrentDictionary
是线程安全的访问。
Answers:
可以以不同的方式查看线程安全集合与非线程安全集合。
考虑一个没有店员的商店,除了结帐时。如果人们不负责任地采取行动,就会遇到很多问题。例如,假设某个客户从金字塔罐中拿出一个罐子,而店员正在建造金字塔时,地狱就会崩溃。或者,如果两个客户同时到达同一商品,谁会赢呢?会吵架吗?这是一个非线程安全的集合。有很多避免问题的方法,但是它们都需要某种锁定,或者以某种方式进行显式访问。
另一方面,考虑一个在桌子上有职员的商店,您只能通过他来购物。您排队,问他一个项目,他把它带回给您,您就不排队了。如果您需要多个物品,则每次往返只能拿起尽可能多的物品,但是您要小心避免挤弄业务员,这会激怒后面的其他顾客。
现在考虑一下。在商店里只有一个店员,如果您一直走到生产线的最前面,问店员“您有卫生纸吗”,然后他说“是”,然后您说“好,我是”当我知道我需要多少时,会尽快回覆给您”,然后当您回到生产线的最前面时,商店当然可以卖光了。线程安全集合不能阻止这种情况。
线程安全集合保证其内部数据结构始终有效,即使从多个线程进行访问也是如此。
非线程安全的集合不附带任何此类保证。例如,如果您在一个线程上向二进制树添加了某些内容,而另一个线程正忙于重新平衡该树,则无法保证将添加该项目,甚至无法保证此树在此之后仍然有效,那么它可能会损坏而无法预期。
但是,线程安全集合不能保证对该线程的顺序操作都可以在其内部数据结构的同一“快照”上进行,这意味着如果您具有以下代码:
if (tree.Count > 0)
Debug.WriteLine(tree.First().ToString());
您可能会收到NullReferenceException,因为在tree.Count
和之间tree.First()
,另一个线程清除了树中的其余节点,这意味着First()
将返回null
。
对于这种情况,您要么需要查看有问题的集合是否具有获取所需内容的安全方法,要么需要重写上面的代码,或者可能需要锁定。
Dictionary
和自己处理锁定与使用ConcurrentDictionary
.NET 4+内置的类型之间的区别。我实际上对此感到困惑。
使用线程安全的集合时,您仍然需要非常小心,因为线程安全并不意味着您可以忽略所有线程问题。当一个集合将自己声明为线程安全的时,通常意味着即使多个线程同时进行读写,它也保持一致状态。但这并不意味着如果一个线程调用多个方法,它将看到“逻辑”结果序列。
例如,如果首先检查某个键是否存在,然后再获取与该键相对应的值,则即使使用ConcurrentDictionary版本,该键也可能不再存在(因为另一个线程可能已删除了该键)。在这种情况下,您仍然需要使用锁定(或者更好的方法是:使用TryGetValue组合两个调用)。
因此,请务必使用它们,但不要认为它给您免费通行证而忽略了所有并发问题。您仍然需要小心。
TryGetValue
它一直是Dictionary
推荐的方法,并且一直是推荐的方法。ConcurrentDictionary
引入的重要“并发”方法是AddOrUpdate
和GetOrAdd
。因此,很好的答案,但可以选择一个更好的例子。
内部ConcurrentDictionary为每个哈希存储区使用单独的锁。只要您仅使用Add / TryGetValue以及适用于单个条目的类似方法,该词典就可以用作几乎无锁的数据结构,并具有各自的出色性能。OTOH枚举方法(包括Count属性)一次锁定所有存储桶,因此,从性能角度来看,它比同步的Dictionary更糟糕。
我会说,只需使用ConcurrentDictionary。
我认为ConcurrentDictionary.GetOrAdd方法正是大多数多线程方案所需要的。
您是否看到过.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并发扩展的正确模式和避免的反模式。这里是。
该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
都已荡然无存。
Dictionary
,因为内部的内部锁定ConcurrentDictionary
现在是浪费和冗余的。 TryAdd
?,AddOrUpdate
?)时会遇到麻烦。 因此,我的建议是:从Dictionary
+ 开始lock
,并保留稍后升级到a的选项,以ConcurrentDictionary
优化性能(如果该选项实际上可行)。在许多情况下,事实并非如此。
我们使用ConcurrentDictionary的缓存集合,也就是再填充每隔1小时,然后通过多个客户端线程读取,类似的解决方案的是这个示例线程安全吗?题。
我们发现,将其更改为 ReadOnlyDictionary可以 提高整体性能。