为什么ConcurrentQueue和ConcurrentDictionary有“Try”方法——TryAdd、TryD
本文关键字:方法 TryAdd TryD Try ConcurrentQueue ConcurrentDictionary 为什么 | 更新日期: 2023-09-27 17:57:03
ConcurrentQueue
TryDequeue
方法。
Queue
只有Dequeue
方法。
在ConcurrentDictionary
没有Add
方法,但我们有TryAdd
方法。
我的问题是:
这些并发收集方法之间有什么区别?为什么它们对于并发集合不同?
对于Dictionary<TKey, TValue>
,假设您将实现自己的逻辑以确保不会输入重复的键。例如
if(!myDictionary.ContainsKey(key)) myDictionary.Add(key, value);
但是,当我们有多个线程时,我们使用并发集合,并且它们可能同时尝试修改字典。
如果两个线程尝试同时执行上述代码,则myDictionary.ContainsKey(key)
可能会为两个线程返回 false,因为它们同时检查并且尚未添加该键。然后他们都尝试添加密钥,但其中一个失败了。
阅读该代码的人不知道它是多线程的,可能会感到困惑。在添加之前,我检查以确保该键不在字典中。那么我是如何得到例外的呢?
ConcurrentDictionary.TryAdd
通过允许您"尝试"添加密钥来解决这个问题。如果它添加该值,则返回true
.如果没有,则返回false
.但它不会做的是与另一个TryAdd
冲突并抛出异常。
您可以通过将Dictionary
包装在类中并在其周围放置lock
语句来自己完成所有这些操作,以确保一次只有一个线程进行更改。 ConcurrentDictionary
只是为你做这件事,而且做得很好。您不必查看其工作方式的所有详细信息 - 您只需知道已考虑多线程即可使用它。
下面是在多线程应用程序中使用类时要查找的详细信息。如果您转到 ConcurrentDictionary 类的文档并滚动到底部,您将看到以下内容:
线程安全
所有公共和受保护成员 ConcurrentDictionary 是线程安全的,可以使用从多个线程并发。但是,成员通过以下方式访问 ConcurrentDictionary 接口之一 实现(包括扩展方法)不保证 线程安全,可能需要由调用方同步。
换句话说,多个线程可以安全地读取和修改集合。
在字典类下,您将看到以下内容:
多个线程线程安全
一个词典可以支持多个 同时读取器,只要集合不被修改。甚至 因此,通过集合进行枚举本质上不是 线程安全过程。在极少数情况下,枚举争辩使用写入访问权限,必须在整个过程中锁定集合枚举。允许多个集合访问 用于读写的线程,您必须实现自己的线程 同步。
可以读取键,但如果多个线程要写入,那么您需要以某种方式lock
字典以确保一次只有一个线程尝试更新。
Dictionary<TKey, TValue>
公开Keys
集合和Values
集合,以便您可以枚举键和值,但它警告您,如果另一个线程要修改字典,请不要尝试这样做。在添加或删除项时,无法枚举某些内容。如果需要循环访问键或值,则必须锁定字典以防止在该迭代期间更新。
ConcurrentDictionary<TKey, TValue>
假定将有多个线程读取和写入,因此它甚至不会公开键或值集合供您枚举。
语义是不同的。
Queue.Dequeue
失败通常表示内部应用程序逻辑存在问题,因此在这种情况下引发异常是很好的。
但是,ConcurrentQueue.TryDeque
失败在常规流中可能是预期的,因此避免异常并返回Boolean
是处理它的合理方法。
ConcurrentQueue<T>
内部处理所有同步。如果两个线程在同一时刻调用TryDequeue
,则两个操作都不会被阻塞。当在两个线程之间检测到冲突时,一个线程必须再次尝试检索下一个元素,并且同步在内部处理。
(在 .NET 框架中,通常的做法是具有返回布尔结果而不是抛出的Try...
函数,例如 TryParse
方法。
这些方法被赋予Try
语义的原因是,根据设计,无法可靠地判断Dequeue
或Add
操作将成功。
当队列不并发时,可以在调用该方法之前检查是否有任何要取消排队的内容Dequeue
。同样,您可以检查非并发Dictionary
中的密钥是否存在。您不能对并发类执行相同的操作,因为有人可能会在您检查项目是否存在之后,但在您实际取消排队之前取消排队。换句话说,Try
操作允许您检查前提条件并以原子方式执行操作。
另一种方法是让您取消排队或添加,并在操作失败时引发异常,就像非并发实现所做的那样。这种方法的缺点是,非并发类中的这些异常情况在并发类中完全是预期的,因此对它们使用异常处理将是错误的。
由于这些集合被设计为并发使用,因此您不能依赖以顺序方式检查前提条件,您需要原子操作。
以字典为例,通常你可以编写这样的代码:
if (!dictionary.ContainsKey(key))
{
dictionary.Add(key, value);
}
在多个线程使用同一字典的情况下,另一个线程完全有可能在您检查ContainsKey
和调用Add
之间插入具有相同键的值。
TryAdd
解决了这个问题,因为它会成功或失败,具体取决于密钥是否存在。
来自 MSDN:
尝试删除并返回开头的对象 并发队列。
返回
如果元素被删除并从 并发队列成功;否则,为假。
因此,如果您可以删除TryDequeue
只需重新移动并返回它,如果不能返回 false,并且您知道在 queeue 空闲时重试。