ConcurrentQueue
有TryDequeue
方法。
Queue
有刚刚的Dequeue
方法。
ConcurrentDictionary
里面没有方法,但Add
我们有。TryAdd
我的问题是:
这些并发收集方法之间有什么区别?为什么它们对于并发集合不同?
ConcurrentQueue
有TryDequeue
方法。
Queue
有刚刚的Dequeue
方法。
ConcurrentDictionary
里面没有方法,但Add
我们有。TryAdd
我的问题是:
这些并发收集方法之间有什么区别?为什么它们对于并发集合不同?
语义不同。
失败Queue.Dequeue
通常表明内部应用程序逻辑有问题,因此在这种情况下抛出异常是好的。
ConcurrentQueue.TryDeque
然而,在常规流程中可能会出现失败,因此避免异常并返回 aBoolean
是处理它的合理方法。
ConcurrentQueue<T>
在内部处理所有同步。如果两个线程TryDequeue
恰好在同一时刻调用,则两个操作都不会被阻塞。当检测到两个线程之间发生冲突时,一个线程必须再次尝试检索下一个元素,并在内部处理同步。
(.NET 框架中的常见做法是具有Try...
返回布尔结果而不是抛出的函数,请参见例如TryParse
方法。)
这些方法被赋予Try
语义的原因是,按照设计,没有办法可靠地判断操作Dequeue
是否Add
会成功。
当队列不并发时,您可以在调用Dequeue
方法之前检查是否有任何要出列的内容。同样,您可以检查非并发中的键Dictionary
是否存在。你不能对并发类做同样的事情,因为有人可能会在你检查它是否存在之后,但在你真正将它出列之前将它出列。换句话说,操作让您检查前提条件并以原子方式Try
执行操作。
另一种方法是让您无论如何都出队或添加,并在操作失败时抛出异常,就像非并发实现一样。这种方法的缺点是非并发类中的这些异常情况完全可以在并发类中预期,因此对它们使用异常处理是错误的。
假设您将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下,您会看到:
线程安全
字典可以同时支持多个 阅读器,只要不修改集合即可。即便如此,通过集合枚举本质上不是线程安全的过程。在枚举 与写访问竞争的极少数情况下,必须在整个 枚举期间锁定集合。要允许集合被多个线程访问以进行读写,您必须实现自己的同步。
多个线程可以读取键,但如果多个线程要写入,那么您需要以某种方式lock
使用字典以确保一次只有一个线程尝试更新。
Dictionary<TKey, TValue>
公开一个Keys
集合和Values
集合,以便您可以枚举键和值,但如果另一个线程要修改字典,它会警告您不要尝试这样做。在添加或删除项目时,您无法枚举某些内容。如果您需要遍历键或值,则必须锁定字典以防止在该迭代期间更新。
ConcurrentDictionary<TKey, TValue>
假设将有多个线程读取和写入,因此它甚至不公开键或值集合供您枚举。
由于这些集合被设计为并发使用,您不能依赖以顺序方式检查前置条件,您需要一个原子操作。
以字典为例,通常你可以这样写代码:
if (!dictionary.ContainsKey(key))
{
dictionary.Add(key, value);
}
在多个线程使用同一个字典的情况下,另一个线程完全有可能在您检查ContainsKey
和调用Add
.
TryAdd
解决了这个问题,因为它会根据密钥是否存在而成功或失败。
来自MSDN:
尝试在并发队列的开头删除并返回对象。
退货
如果一个元素被移除并从 ConcurrentQueue 的开头成功返回,则为 true;否则为假。
因此,如果您可以删除TryDequeue
reomove 并返回它,如果不能返回 false 并且您知道在队列空闲时再试一次。