为什么ConcurrentQueue和ConcurrentDictionary有“Try”方法 - TryAdd,TryDequeue - 而不是Add和Dequeue?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了为什么ConcurrentQueue和ConcurrentDictionary有“Try”方法 - TryAdd,TryDequeue - 而不是Add和Dequeue?相关的知识,希望对你有一定的参考价值。

ConcurrentQueueTryDequeue方法。

Queue只有Dequeue方法。

ConcurrentDictionary没有Add方法,但我们有TryAdd而不是。

我的问题是:

这些并发收集方法有什么区别?为什么它们对于并发集合有所不同?

答案

使用Dictionary<TKey, TValue>,假设您将实现自己的逻辑以确保不输入重复键。例如,

if(!myDictionary.ContainsKey(key)) myDictionary.Add(key, value);

但是当我们有多个线程时,我们使用Concurrent集合,并且它们可能同时尝试修改字典。

如果两个线程同时尝试执行上面的代码,那么myDictionary.ContainsKey(key)可能会为两个线程返回false,因为它们同时检查并且尚未添加该密钥。然后他们都试图添加密钥,一个失败。

阅读那些不知道它是多线程的代码的人可能会感到困惑。我检查以确保在添加密钥之前密钥不在字典中。那我怎么得到一个例外?

ConcurrentDictionary.TryAdd通过允许您“尝试”添加密钥来解决这个问题。如果它添加了值,则返回true。如果不是,则返回false。但它不会做的是与另一个TryAdd冲突并抛出异常。

你可以通过将Dictionary包装在一个类中并在其周围放置lock语句来确保所有这一切,以确保一次只有一个线程进行更改。 ConcurrentDictionary只为你做到这一点并且做得非常好。您不必查看其工作原理的所有细节 - 您只需使用它就知道已经考虑了多线程。

以下是在多线程应用程序中使用类时要查找的详细信息。如果你去ConcurrentDictionary Class的文档并滚动到底部你会看到:

线程安全 ConcurrentDictionary的所有公共成员和受保护成员都是线程安全的,可以从多个线程同时使用。但是,通过ConcurrentDictionary实现的其中一个接口访问的成员(包括扩展方法)不能保证是线程安全的,并且可能需要由调用者同步。

换句话说,多个线程可以安全地读取和修改集合。

Dictionary Class下你会看到这个:

线程安全 只要未修改集合,Dictionary就可以同时支持多个读取器。即便如此,通过集合枚举本质上不是一个线程安全的过程。在枚举与写访问争用的极少数情况下,必须在整个枚举期间锁定该集合。要允许多个线程访问集合以进行读写,您必须实现自己的同步。

多个线程可以读取密钥,但是如果要写多个线程,那么你需要以某种方式lock字典,以确保一次只有一个线程尝试更新。

Dictionary<TKey, TValue>公开了一个Keys集合和Values集合,因此你可以枚举键和值,但它警告你不要尝试这样做,如果另一个线程将要修改字典。在添加或删除项目时,您无法枚举某些内容。如果需要遍历键或值,则必须锁定字典以防止在该迭代期间进行更新。

ConcurrentDictionary<TKey, TValue>假设将有多个线程读写,因此它甚至不会公开键或值集合供您枚举。

另一答案

语义是不同的。

Queue.Dequeue的失败通常表明内部应用程序逻辑存在问题,因此在这种情况下抛出异常是好的。

然而,ConcurrentQueue.TryDeque的失败是可以在常规流程中预期的,因此避免异常并返回Boolean是处理它的合理方法。

ConcurrentQueue<T>在内部处理所有同步。如果两个线程在同一时刻调用TryDequeue,则两个操作都不会被阻止。当在两个线程之间检测到冲突时,一个线程必须再次尝试检索下一个元素,并在内部处理同步。

(在.NET框架中通常的做法是使用Try...函数返回布尔结果而不是抛出,参见例如TryParse方法。)

另一答案

这些方法被赋予Try语义的原因是,根据设计,没有办法可靠地告诉DequeueAdd操作将成功。

当队列不是并发时,您可以在调用Dequeue方法之前检查是否有任何要出队的内容。同样,您可以检查非并发Dictionary中的密钥是否存在。你不能对并发类做同样的事情,因为有人可能会在你检查它之后将你的项目出列,但是在你实际出列之前。换句话说,Try操作允许您检查前提条件并以原子方式执行操作。

另一种方法是让你出队或添加,并在操作失败时抛出异常,就像非并发实现一样。这种方法的缺点是在并发类中完全需要非并发类中的这些异常情况,因此对它们使用异常处理将是错误的。

另一答案

由于这些集合旨在同时使用,因此您不能依赖于以顺序方式检查前置条件,而是需要进行原子操作。

以字典为例,通常你可以编写这样的代码:

if (!dictionary.ContainsKey(key))
{
    dictionary.Add(key, value);
}

在多个线程使用相同字典的情况下,另一个线程完全有可能在检查ContainsKey和调用Add之间插入一个具有相同键的值。

TryAdd解决了这个问题,因为它会成功还是失败,具体取决于密钥是否存在。

另一答案

来自MSDN

尝试在并发队列的开头删除并返回对象。

返回

如果一个元素被删除并从ConcurrentQueue的开头成功返回,则为true;否则,错误。

因此,如果你可以删除qazxsw poi只是删除并返回它,如果不能返回false,你知道在队列空闲时再试一次。

以上是关于为什么ConcurrentQueue和ConcurrentDictionary有“Try”方法 - TryAdd,TryDequeue - 而不是Add和Dequeue?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用ConcurrentQueue进行线程处理

关于 C# ConcurrentQueue<T>

ConcurrentQueue队列

evpp性能测试: 对无锁队列boost::lockfree::queue和moodycamel::ConcurrentQueue做一个性能对比测试

evpp性能测试: 对无锁队列boost::lockfree::queue和moodycamel::ConcurrentQueue做一个性能对比测试

C#多线程开发-并发集合中的ConcurrentQueue