为什么ConcurrentQueue和ConcurrentDictionary有"Try"方法——TryAdd、TryDequeue,而不是Add和Dequeue?

4

ConcurrentQueueTryDequeue方法。

Queue只有Dequeue方法。

ConcurrentDictionary中没有Add方法,但我们有TryAdd代替。

我的问题是:

这些并发集合方法之间有什么区别?为什么并发集合的方法是不同的?


由于它们确保线程安全访问,如果当前线程无法访问字典,则tryadd可能会失败。 - apomene
5个回答

5

使用Dictionary<TKey, TValue>时,需要自己实现逻辑来确保不会输入重复的键。例如:

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

当我们有多个线程运行且可能同时尝试修改字典时,我们使用并发集合。

如果两个线程同时执行上述代码,则它们都在同时检查并未添加该键值对,因此myDictionary.ContainsKey(key)可能会同时返回 false。 然后它们都尝试添加键,但只有一个成功了。

如果不知道这是多线程的代码,则会让读者感到困惑。我在添加之前检查确保键还不存在,那么为什么会引发异常?

ConcurrentDictionary.TryAdd解决了这个问题,它允许您“尝试”添加键值对。 如果添加成功,则返回true;如果添加失败,则返回false。但是它不会与另一个TryAdd产生冲突并引发异常。

您可以通过将Dictionary包装在类中并在其周围放置lock语句来自己完成所有这些操作。但是,ConcurrentDictionary已经替你做到了这一点,并且做得非常好。您不必看到它的详细工作原理-您只需使用它,知道已经考虑了多线程问题。

在使用多线程应用程序中的类时,请注意以下细节。如果您转到ConcurrentDictionary Class文档并向下滚动,您将看到以下内容:

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

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

Dictionary Class下,您会看到以下内容:

线程安全
一个字典可以支持多个读者并发操作,只要这个集合没有被修改。尽管如此,枚举一个集合本质上不是线程安全的过程。在罕见情况下,如果一个枚举与写访问竞争,那么在整个枚举期间必须锁定该集合。为了允许多个线程对集合进行读写访问,您必须实现自己的同步。
多个线程可以读取键,但是如果多个线程要写入,则需要以某种方式锁定字典,以确保每次只有一个线程尝试更新。
Dictionary 公开了Keys集合和Values集合,以便您可以枚举键和值,但它警告您不要在另一个线程修改字典时尝试执行此操作。如果正在添加或删除项,则无法枚举某些内容。如果需要遍历键或值,则必须锁定字典以防止在迭代期间进行更新。
ConcurrentDictionary 假定将有多个线程读取和写入,因此甚至不会公开键或值集合供您枚举。

4
语义不同。 Queue.Dequeue 失败通常表示应用程序内部逻辑存在问题,因此在这种情况下抛出异常是好的。
然而,ConcurrentQueue.TryDeque 的失败可能在正常流程中是可以预期的,因此避免抛出异常并返回一个Boolean 是一种合理的处理方式。

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

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

4
这些方法被称为Try语义是因为按照设计,没有可靠的方法来确定DequeueAdd操作是否成功。
当队列不是并发时,可以在调用Dequeue方法之前检查是否有任何内容可以出列。同样,可以检查非并发的Dictionary中是否存在键。但是在并发类中,您不能这样做,因为在您检查它是否存在之后,其他人可能会将您的项从队列中删除。换句话说,Try操作允许您原子地检查前提条件并执行该操作。
另一种方法是让您无论如何都可以出列或添加,并在操作失败时抛出异常,就像非并发实现那样。采用此方法的缺点是,非并发类中的这些异常情况在并发类中完全是预期的,因此对它们使用异常处理是错误的。

3

由于这些集合是同时设计使用的,因此您不能依赖于按顺序检查前提条件,您需要进行原子操作。

以字典为例,通常您可以编写如下代码:

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

在多线程使用同一个字典的情况下,当你在检查`ContainsKey`和调用`Add`之间,另一个线程完全有可能插入了一个具有相同键的值。
`TryAdd`解决了这个问题,它会根据键值是否存在来成功或失败。

0

来自MSDN

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

返回

如果成功从ConcurrentQueue的开头移除并返回元素,则返回true;否则返回false。

因此,如果您可以删除TryDequeue,只需删除并返回它,如果无法删除,则返回false,并在队列空闲时再次尝试。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接