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

Why do ConcurrentQueue and ConcurrentDictionary have "Try" methods - TryAdd, TryDequeue - instead of Add and Dequeue?

ConcurrentQueueTryDequeue 方法。

Queue 只有 Dequeue 方法。

ConcurrentDictionary中没有Add方法,但是我们有TryAdd

我的问题是:

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

语义不同。

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

但是 ConcurrentQueue.TryDeque 的失败在常规流程中可能是预期的事情,因此避免异常并 returning a Boolean 是处理它的合理方法。

ConcurrentQueue<T> handles all synchronization internally. If two threads call TryDequeue at precisely the same moment, neither operation is blocked. When a conflict is detected between two threads, one thread has to try again to retrieve the next element, and the synchronization is handled internally.

(.NET Framework 中的常见做法是具有 return 布尔结果而不是抛出的 Try... 函数,参见例如 TryParse 方法。)

这些方法被赋予 Try 语义的原因是,根据设计,无法可靠地判断 DequeueAdd 操作将成功。

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

执行操作

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

由于这些集合被设计为并发使用,您不能依赖于以顺序方式检查前提条件,您需要一个原子操作。

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

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

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

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

来自MSDN

Tries to remove and return the object at the beginning of the concurrent queue.

Returns

true if an element was removed and returned from the beginning of the ConcurrentQueue successfully; otherwise, false.

因此,如果您可以删除 TryDequeue,只需重新删除并 return 它,如果不能 returns false 并且您知道在队列空闲时重试。

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

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

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

如果两个线程试图同时执行上面的代码,myDictionary.ContainsKey(key) 可能对两个线程都是 return false,因为它们同时检查那个键尚未添加。然后他们都尝试添加密钥,但其中一个失败了。

阅读该代码但不知道它是多线程的人可能会感到困惑。我检查以确保在 添加之前 字典中不存在密钥。那么我如何获得异常?

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

您可以通过将 Dictionary 包装在 class 中并在其周围放置 lock 语句来确保一次只有一个线程进行更改,从而自行完成所有这些操作。 ConcurrentDictionary 就是为您做的,而且做得非常好。您不必查看其工作原理的所有详细信息 - 您只需在知道多线程已被考虑在内的情况下使用它。

这是在多线程应用程序中使用 class 时要查找的详细信息。如果您转到 ConcurrentDictionary Class 的文档并滚动到底部,您将看到:

Thread Safety
All public and protected members of ConcurrentDictionary are thread-safe and may be used concurrently from multiple threads. However, members accessed through one of the interfaces the ConcurrentDictionary implements, including extension methods, are not guaranteed to be thread safe and may need to be synchronized by the caller.

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

Dictionary Class下你会看到这个:

Thread Safety
A Dictionary can support multiple readers concurrently, as long as the collection is not modified. Even so, enumerating through a collection is intrinsically not a thread-safe procedure. In the rare case where an enumeration contends with write accesses, the collection must be locked during the entire enumeration. To allow the collection to be accessed by multiple threads for reading and writing, you must implement your own synchronization.

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

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

ConcurrentDictionary<TKey, TValue>假设会有多个线程读写,所以它甚至不会暴露键或值集合供您枚举。