【问题标题】:Why do ConcurrentQueue and ConcurrentDictionary have "Try" methods - TryAdd, TryDequeue - instead of Add and Dequeue?为什么 ConcurrentQueue 和 ConcurrentDictionary 有“Try”方法 - TryAdd、TryDequeue - 而不是 Add 和 Dequeue?
【发布时间】:2016-11-18 16:27:21
【问题描述】:

ConcurrentQueueTryDequeue 方法。

Queue 仅有 Dequeue 方法。

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

我的问题是:

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

【问题讨论】:

  • 由于他们保证线程安全访问,如果当前线程无法访问字典,tryadd可能会失败

标签: c# collections concurrentdictionary concurrent-queue


【解决方案1】:

语义不同。

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

不过,ConcurrentQueue.TryDeque 的失败在常规流程中可能会发生,因此避免异常并返回 Boolean 是一种合理的处理方式。

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

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

【讨论】:

    【解决方案2】:

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

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

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

    【讨论】:

      【解决方案3】:

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

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

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

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

      TryAdd 解决了这个问题,因为它会根据密钥是否存在而成功或失败。

      【讨论】:

        【解决方案4】:

        来自MSDN

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

        返回

        如果一个元素被删除并从开头返回,则为真 并发队列成功;否则为假。

        因此,如果您可以删除 TryDequeue,只需 reomove 并返回它,如果不能返回 false 并且您知道在队列空闲时再试一次。

        【讨论】:

          【解决方案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 下你会看到:

          线程安全
          一个字典可以支持多个 读者并发,只要集合没有被修改。甚至 因此,通过集合枚举本质上不是 线程安全的过程。 在极少数情况下枚举争用 对于写访问,集合必须在整个过程中被锁定 枚举。允许多个人访问该集合 读写线程,你必须实现你自己的 同步。

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

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

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

          【讨论】:

            猜你喜欢
            • 2012-03-18
            • 2022-10-18
            • 2013-09-07
            • 1970-01-01
            • 1970-01-01
            • 2016-04-06
            • 2013-03-30
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多