同時プライオリティキューを使用する必要があり、MSDNのHow to: Add Bounding and Blocking Functionality to a CollectionチュートリアルのSimplePriorityQueue<TPriority, TValue>
サンプルを適用することを検討していました。しかし、私は、そのサンプルが持つと思われるバグの重大さに驚いた。誰かがこれらの問題が本当に存在するかどうかを検証できますか?MSDNのSimplePriorityQueueサンプルの重大なバグ
1)レースハザードがArgumentException
後者からスローさせることができるTryAdd
とToArray
、間に存在します。 TryAdd
メソッドは、最初に項目を内部キューに追加してから、m_count
カウンタをインクリメントします。一方、ToArray
は、最初にサイズm_count
の新しい配列を初期化し、内部配列をこの配列にコピーします。 ToArray
が実行されているときにTryAdd
が呼び出された場合、ToArray
は、CopyTo
コールがArgumentException
をスローする原因となる、配列内のスペースを割り当てた以上のアイテムのコピーを試みる可能性があります。
private ConcurrentQueue<KeyValuePair<int, TValue>>[] _queues;
private int m_count;
// ...
// IProducerConsumerCollection members
public bool TryAdd(KeyValuePair<int, TValue> item)
{
_queues[item.Key].Enqueue(item);
Interlocked.Increment(ref m_count);
return true;
}
public int Count
{
get { return m_count; }
}
public KeyValuePair<int, TValue>[] ToArray()
{
KeyValuePair<int, TValue>[] result;
lock (_queues)
{
result = new KeyValuePair<int, TValue>[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<int, TValue>> GetEnumerator()
{
for (int i = 0; i < priorityCount; i++)
{
foreach (var item in _queues[i])
yield return item;
}
}
キューからアイテムを取り、インクリメント優先でそれを再追加し、次のコードスニペット、考えてみましょう:上記のスニペットを列挙して同時に実行された場合は、1が期待する
if (queue.TryTake(out item) && item.Key < maxPriority - 1)
queue.TryAdd(new KeyValuePair<int, string>(item.Key + 1, item.Value))
をアイテムは、元のアイテムまたは増分された優先度のいずれかで最大で1回表示されるか、場合によってはまったく表示されません。その商品がの2倍のの優先度で表示されるとは思わないでしょう。ただし、GetEnumerator
は内部キューを順番に反復処理するため、キュー全体でこのような順序の不一致を防ぎません。それは、任意のメモリフェンスなし共有m_count
フィールドを読み取るため
3)パブリックCount
性は、古くなった値を返すことができます。コンシューマが以下のような独自のメモリフェンスを生成しないループでこのプロパティにアクセスすると、他のスレッドによってアイテムがキューに追加されているにもかかわらず、無限ループに陥る可能性があります。
while (queue.Count == 0)
{ }
共有変数を読み取るときは、いくつかの他の記事で議論されたメモリフェンスの必要性:
- How to correctly read an Interlocked.Increment'ed int field?
- Reading an int that's updated by Interlocked on other threads
- Do concurrent interlocked and reads require a memory barrier or locking?
4) THER eは、_queues
アレイの初期化とSimplePriorityQueue
コンストラクタの完了との間のメモリバリアではありません。競合の危険は、別のスレッドの外部消費者がTryAdd
と呼び出し、_queues
にアクセスしてからにアクセスすると発生する可能性があります(またはメモリキャッシュに完了したと表示されます)。これは、constructors and memory barriersの私の他の質問でさらに議論されています。
5)TryTake
とToArray
はlock
キーワードの使用によって保護されています。不十分であることとは別に(上記で説明したバグのために)、これは並行コレクションの設計の全目的を破っています。その短所を考えると、内部の構造体を平文Queue
にダウングレードし、どこにいてもロックを追加し、これを並行ではなくスレッドセーフな構造として扱うことが最善のアプローチだと思います。
はい、MSDNの例が必ずしも最大であるとは限りません。 –
@ScottChamberlain:True; [volatile](https://msdn.microsoft.com/en-us/library/x13ttww7.aspx)キーワードのような並列性に関する基本的な話題でさえ、まったくの欠点があります。しかし、私は上記のようにMSDNでこのような混乱を見たことはありません。 – Douglas