複数のスレッドがキューに追加され、複数のスレッドが同じキューから読み取るシナリオがあります。キューが特定のサイズに達した場合すべてのスレッドは、キューから項目が削除されるまで、追加時にブロックされます。
以下の解決策は私が今使用しているものであり、私の質問は次のとおりです:これをどのように改善できますか?使用する必要があるBCLでこの動作を既に有効にしているオブジェクトはありますか?
internal class BlockingCollection<T> : CollectionBase, IEnumerable
{
//todo: might be worth changing this into a proper QUEUE
private AutoResetEvent _FullEvent = new AutoResetEvent(false);
internal T this[int i]
{
get { return (T) List[i]; }
}
private int _MaxSize;
internal int MaxSize
{
get { return _MaxSize; }
set
{
_MaxSize = value;
checkSize();
}
}
internal BlockingCollection(int maxSize)
{
MaxSize = maxSize;
}
internal void Add(T item)
{
Trace.WriteLine(string.Format("BlockingCollection add waiting: {0}", Thread.CurrentThread.ManagedThreadId));
_FullEvent.WaitOne();
List.Add(item);
Trace.WriteLine(string.Format("BlockingCollection item added: {0}", Thread.CurrentThread.ManagedThreadId));
checkSize();
}
internal void Remove(T item)
{
lock (List)
{
List.Remove(item);
}
Trace.WriteLine(string.Format("BlockingCollection item removed: {0}", Thread.CurrentThread.ManagedThreadId));
}
protected override void OnRemoveComplete(int index, object value)
{
checkSize();
base.OnRemoveComplete(index, value);
}
internal new IEnumerator GetEnumerator()
{
return List.GetEnumerator();
}
private void checkSize()
{
if (Count < MaxSize)
{
Trace.WriteLine(string.Format("BlockingCollection FullEvent set: {0}", Thread.CurrentThread.ManagedThreadId));
_FullEvent.Set();
}
else
{
Trace.WriteLine(string.Format("BlockingCollection FullEvent reset: {0}", Thread.CurrentThread.ManagedThreadId));
_FullEvent.Reset();
}
}
}
これは非常に安全ではないように見えます(同期はほとんどありません)。次のようなものはどうですか:
class SizeQueue<T>
{
private readonly Queue<T> queue = new Queue<T>();
private readonly int maxSize;
public SizeQueue(int maxSize) { this.maxSize = maxSize; }
public void Enqueue(T item)
{
lock (queue)
{
while (queue.Count >= maxSize)
{
Monitor.Wait(queue);
}
queue.Enqueue(item);
if (queue.Count == 1)
{
// wake up any blocked dequeue
Monitor.PulseAll(queue);
}
}
}
public T Dequeue()
{
lock (queue)
{
while (queue.Count == 0)
{
Monitor.Wait(queue);
}
T item = queue.Dequeue();
if (queue.Count == maxSize - 1)
{
// wake up any blocked enqueue
Monitor.PulseAll(queue);
}
return item;
}
}
}
(編集)
実際には、読者がきれいに終了できるようにキューを閉じる方法が必要です-おそらくboolフラグのようなもの-設定されている場合、空のキューが(ブロッキングではなく)戻ります:
bool closing;
public void Close()
{
lock(queue)
{
closing = true;
Monitor.PulseAll(queue);
}
}
public bool TryDequeue(out T value)
{
lock (queue)
{
while (queue.Count == 0)
{
if (closing)
{
value = default(T);
return false;
}
Monitor.Wait(queue);
}
value = queue.Dequeue();
if (queue.Count == maxSize - 1)
{
// wake up any blocked enqueue
Monitor.PulseAll(queue);
}
return true;
}
}
.net 4 BlockingCollectionを使用して、Add()の使用をキューに入れ、Take()の使用をキューから外します。内部的にノンブロッキングConcurrentQueueを使用します。詳細はこちら 高速および最適なプロデューサー/コンシューマーキューテクニックBlockingCollection vs Concurrent Queue
「これをどのように改善できますか?」
さて、クラス内のすべてのメソッドを調べて、別のスレッドがそのメソッドまたは他のメソッドを同時に呼び出している場合にどうなるかを考慮する必要があります。たとえば、Removeメソッドにはロックを設定しますが、Addメソッドにはロックを設定しません。あるスレッドが別のスレッドの削除と同時に追加されるとどうなりますか? 悪いこと
また、メソッドは、最初のオブジェクトの内部データへのアクセスを提供する2番目のオブジェクト(GetEnumeratorなど)を返すこともできます。あるスレッドがその列挙子を通過し、別のスレッドが同時にリストを変更していると想像してください。 良くない
良い経験則は、クラス内のメソッドの数を絶対的な最小値に減らして、これをより簡単にすることです。
特に、別のコンテナクラスを継承しないでください。そのクラスのすべてのメソッドを公開し、呼び出し元が内部データを破損したり、データの部分的に完全な変更を確認したりする方法を提供するためですその時点で破損しているように見えます)。すべての詳細を非表示にし、それらへのアクセスを許可する方法について完全に冷酷です。
既製のソリューションを使用することを強くお勧めします-スレッド化に関する本を入手するか、サードパーティのライブラリを使用します。さもなければ、あなたがしようとしていることを考えると、あなたは長い間コードをデバッグすることになります。
また、発信者が特定のアイテムを選択するよりも、Removeがアイテム(たとえば、キューであるために最初に追加されたアイテム)を返す方が理にかなっていますか?また、キューが空の場合、おそらくRemoveもブロックする必要があります。
更新:Marcの答えは、実際にこれらすべての提案を実装しています! :)しかし、彼のバージョンがこのように改善されている理由を理解するのに役立つかもしれないので、ここに残しておきます。
System.Collections.Concurrent名前空間で BlockingCollection および ConcurrentQueue を使用できます
public class ProducerConsumerQueue<T> : BlockingCollection<T>
{
/// <summary>
/// Initializes a new instance of the ProducerConsumerQueue, Use Add and TryAdd for Enqueue and TryEnqueue and Take and TryTake for Dequeue and TryDequeue functionality
/// </summary>
public ProducerConsumerQueue()
: base(new ConcurrentQueue<T>())
{
}
/// <summary>
/// Initializes a new instance of the ProducerConsumerQueue, Use Add and TryAdd for Enqueue and TryEnqueue and Take and TryTake for Dequeue and TryDequeue functionality
/// </summary>
/// <param name="maxSize"></param>
public ProducerConsumerQueue(int maxSize)
: base(new ConcurrentQueue<T>(), maxSize)
{
}
}
私はReactive Extensionsを使用してこれをノックアップし、この質問を思い出しました:
public class BlockingQueue<T>
{
private readonly Subject<T> _queue;
private readonly IEnumerator<T> _enumerator;
private readonly object _sync = new object();
public BlockingQueue()
{
_queue = new Subject<T>();
_enumerator = _queue.GetEnumerator();
}
public void Enqueue(T item)
{
lock (_sync)
{
_queue.OnNext(item);
}
}
public T Dequeue()
{
_enumerator.MoveNext();
return _enumerator.Current;
}
}
必ずしも完全に安全ではありませんが、非常に簡単です。
これは、スレッドセーフな制限付きブロッキングキューを操作するために来たものです。
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
public class BlockingBuffer<T>
{
private Object t_lock;
private Semaphore sema_NotEmpty;
private Semaphore sema_NotFull;
private T[] buf;
private int getFromIndex;
private int putToIndex;
private int size;
private int numItems;
public BlockingBuffer(int Capacity)
{
if (Capacity <= 0)
throw new ArgumentOutOfRangeException("Capacity must be larger than 0");
t_lock = new Object();
buf = new T[Capacity];
sema_NotEmpty = new Semaphore(0, Capacity);
sema_NotFull = new Semaphore(Capacity, Capacity);
getFromIndex = 0;
putToIndex = 0;
size = Capacity;
numItems = 0;
}
public void put(T item)
{
sema_NotFull.WaitOne();
lock (t_lock)
{
while (numItems == size)
{
Monitor.Pulse(t_lock);
Monitor.Wait(t_lock);
}
buf[putToIndex++] = item;
if (putToIndex == size)
putToIndex = 0;
numItems++;
Monitor.Pulse(t_lock);
}
sema_NotEmpty.Release();
}
public T take()
{
T item;
sema_NotEmpty.WaitOne();
lock (t_lock)
{
while (numItems == 0)
{
Monitor.Pulse(t_lock);
Monitor.Wait(t_lock);
}
item = buf[getFromIndex++];
if (getFromIndex == size)
getFromIndex = 0;
numItems--;
Monitor.Pulse(t_lock);
}
sema_NotFull.Release();
return item;
}
}
TPL を完全に調査したわけではありませんが、彼らはあなたのニーズに合った何かを持っているかもしれません。
お役に立てば幸いです。
さて、あなたはSystem.Threading.Semaphore
クラスを見るかもしれません。それ以外-いいえ、あなたはこれを自分で作らなければなりません。私の知る限り、そのような組み込みのコレクションはありません。