MSDN上的SimplePriorityQueue示例中存在严重错误
我需要使用并发优先级队列,我正在考虑在MSDN上调整如何:添加边界和阻塞function中给出的SimplePriorityQueue
示例到一个收集教程。 但是,我对这个样本似乎有的错误的严重性感到惊讶。 有人可以validation这些问题是否确实存在?
1) TryAdd
和ToArray
之间存在竞争危险,这可能导致从后者抛出ArgumentException
。 TryAdd
方法首先将项添加到内部队列,然后递增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
关键字来保护TryTake
和ToArray
。 除了不充分(由于上面讨论的错误),这也破坏了设计并发集合的整个目的。 鉴于它的缺点,我认为最好的方法是将内部ConcurrentQueue
结构降级为plain Queue
,在任何地方添加锁,并开始将其视为非并发但线程安全的结构。
我认为这取决于你如何使用这门课程。 如果你将自己TryTake
和TryTake
(这是BlockingCollection
依赖的两个主要内容),那么你应该没有任何问题,并且你将拥有一个非常快速的锁定最小优先级队列。
如果您开始使用Count
, CopyTo
或任何其他方法,您可能会遇到您指出的问题。