MSDN上的SimplePriorityQueue示例中存在严重错误

我需要使用并发优先级队列,我正在考虑在MSDN上调整如何:添加边界和阻塞function中给出的SimplePriorityQueue示例到一个收集教程。 但是,我对这个样本似乎有的错误的严重性感到惊讶。 有人可以validation这些问题是否确实存在?

1) TryAddToArray之间存在竞争危险,这可能导致从后者抛出ArgumentExceptionTryAdd方法首先将项添加到内部队列,然后递增m_count计数器。 另一方面, ToArray首先初始化一个大小为m_count的新数组,然后将内部队列复制到该数组中。 如果在执行ToArray调用ToArray ,则ToArray可能最终会尝试复制比在数组中分配空间更多的项目,从而导致CopyTo调用抛出ArgumentException

 private ConcurrentQueue<KeyValuePair>[] _queues; private int m_count; // ... // IProducerConsumerCollection members public bool TryAdd(KeyValuePair item) { _queues[item.Key].Enqueue(item); Interlocked.Increment(ref m_count); return true; } public int Count { get { return m_count; } } public KeyValuePair[] ToArray() { KeyValuePair[] result; lock (_queues) { result = new KeyValuePair[this.Count]; // *** context switch here; new item gets added *** int index = 0; foreach (var q in _queues) { if (q.Count > 0) { q.CopyTo(result, index); // *** ArgumentException *** index += q.Count; } } return result; } } 

2) GetEnumerator方法中存在另一种竞争危险:内部队列的更新之间没有同步。

 public IEnumerator<KeyValuePair> GetEnumerator() { for (int i = 0; i < priorityCount; i++) { foreach (var item in _queues[i]) yield return item; } } 

请考虑以下代码片段,该代码片段从队列中获取一个项目并以递增的优先级重新添加它:

 if (queue.TryTake(out item) && item.Key < maxPriority - 1) queue.TryAdd(new KeyValuePair(item.Key + 1, item.Value)) 

如果上面的代码片段与枚举同时运行,则可以预期该项目最多只出现一次,无论是原始片段还是增加的优先级 – 或者可能根本不显示。 在两个优先事项中,人们都不希望该项目出现两次 。 但是,由于GetEnumerator按顺序迭代其内部队列,因此无法防止队列之间的这种排序不一致。

3)公共Count属性可以返回过时值,因为它读取共享m_count字段而没有任何内存栅栏。 如果消费者在不生成自己的内存围栏的循环中访问此属性(如下所示),则它们可能会陷入无限循环,尽管其他线程将项目添加到队列中。

 while (queue.Count == 0) { } 

在阅读共享变量时需要内存屏障已在其他几篇文章中讨论过:

  • 如何正确读取Interlocked.Increment’ed int字段?
  • 读取由Interlocked在其他线程上更新的int
  • 并发互锁和读取是否需要内存屏障或锁定?

4) _queues数组的初始化和SimplePriorityQueue构造函数的完成之间没有内存障碍。 当另一个线程上的外部使用者在初始化完成之前调用_queues并访问_queues (或在其内存缓存中显示为已完成)时,可能会出现种族危险。 在我关于构造函数和内存障碍的另一个问题中进一步讨论了这一点。

5)通过使用lock关键字来保护TryTakeToArray 。 除了不充分(由于上面讨论的错误),这也破坏了设计并发集合的整个目的。 鉴于它的缺点,我认为最好的方法是将内部ConcurrentQueue结构降级为plain Queue ,在任何地方添加锁,并开始将其视为非并发但线程安全的结构。

我认为这取决于你如何使用这门课程。 如果你将自己TryTakeTryTake (这是BlockingCollection依赖的两个主要内容),那么你应该没有任何问题,并且你将拥有一个非常快速的锁定最小优先级队列。

如果您开始使用CountCopyTo或任何其他方法,您可能会遇到您指出的问题。