.NETでブロッキングQueue <T>を作成しますか?


163

複数のスレッドがキューに追加され、複数のスレッドが同じキューから読み取るシナリオがあります。キューが特定のサイズに達すると、キューから項目が削除されるまで、キューを満たしているすべてのスレッドが追加時にブロックされます。

以下の解決策は私が現在使用しているものであり、私の質問は次のとおりです。これをどのように改善できますか?使用する必要がある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();
        }
    }
}

5
.Netには、このシナリオに役立つ組み込みクラスがあります。ここにリストされている答えのほとんどは時代遅れです。下部にある最新の回答をご覧ください。スレッドセーフなブロッキングコレクションを調べます。答えは時代遅れかもしれませんが、それでも良い質問です!
トムA

.NETに新しい同時実行クラスがある場合でも、Monitor.Wait / Pulse / PulseAllについて学ぶことは、まだ良い考えだと思います。
thewpfguy 2014年

1
@thewpfguyに同意します。舞台裏の基本的なロックメカニズムを理解する必要があります。Systems.Collections.Concurrentは2010年4月まで存在せず、その後はVisual Studio 2010以降でのみ存在したことにも注意してください。間違いなくVS2008ホールドアウトのオプションではありません...
Vic

これを今読んでいる場合は、.NET Coreおよび.NET Standard向けの、これのマルチライター/マルチリーダー、制限付き、オプションでブロックする実装のSystem.Threading.Channelsを見てください。
Mark

回答:


200

これは非常に安全ではないように見えます(ほとんど同期していません)。次のようなものはどうですか:

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 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;
    }
}

1
待機をWaitAnyに変更し、構築時に終了待機ハンドルを渡すのはどうですか?
サムサフラン

1
@Marc-キューが常に容量に到達することを期待している場合の最適化は、maxSize値をQueue <T>のコンストラクターに渡すことです。そのために、クラスに別のコンストラクターを追加できます。
RichardOD 2010年

3
なぜSizeQueueなのか、FixedSizeQueueなのか?
mindless.panda

4
@Lasse- Wait他のスレッドがそれを取得できるように、それはの間にロックを解放します。ウェイクアップすると、ロックを再利用します。
Marc Gravell

1
いいですね、私が言ったように、私は得ていなかった何かがありました:)それは確かに私に私のスレッドコードのいくつかを再訪したいと思います...
Lasse V. Karlsen


14

「どうすればこれを改善できますか?」

さて、クラス内のすべてのメソッドを見て、別のスレッドがそのメソッドまたは他のメソッドを同時に呼び出した場合にどうなるかを検討する必要があります。たとえば、Removeメソッドにはロックを設定しましたが、Addメソッドにはロックを設定しませんでした。1つのスレッドが別のスレッドが削除すると同時に追加するとどうなりますか?悪いこと。

また、メソッドが最初のオブジェクトの内部データへのアクセスを提供する2番目のオブジェクト(たとえば、GetEnumerator)を返す可能性があることも考慮してください。あるスレッドがその列挙子を通過し、別のスレッドが同時にリストを変更していると想像してください。良くない。

経験則としては、クラス内のメソッドの数を最小限に抑えることで、これを簡単に正しく行うことができます。

特に、別のコンテナークラスを継承しないでください。そのクラスのすべてのメソッドが公開され、呼び出し元が内部データを破壊したり、データへの部分的な完全な変更を確認したりできるためです(データがその時点で破損しているように見えます)。すべての詳細を非表示にし、どのようにそれらへのアクセスを許可するかについて完全に冷酷にしてください。

既製のソリューションを使用することを強くお勧めします。スレッドに関する本を入手するか、サードパーティのライブラリを使用してください。そうでなければ、あなたがしようとしていることを考えると、あなたは長い間あなたのコードをデバッグするでしょう。

また、呼び出し側が特定のアイテムを選択するのではなく、Removeがアイテム(たとえば、キュ​​ーであるため最初に追加されたアイテム)を返すほうが理にかなっているのではないでしょうか。キューが空の場合、おそらくRemoveもブロックするはずです。

更新:Marcの答えは実際にこれらすべての提案を実装しています!:)しかし、彼のバージョンがなぜそのような改善であるかを理解するのに役立つかもしれないので、これはここに残しておきます。


12

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)
    {
    }



}

3
BlockingCollectionのデフォルトはQueueです。だから、これは必要ないと思います。
Curtis White

BlockingCollectionはキューのような順序を保持しますか?
joelc

はい、ConcurrentQueueで初期化されると
Andreas

6

私は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;
    }
}

完全に安全である必要はありませんが、非常に単純です。


件名とは<t>?名前空間のリゾルバがありません。
theJerm 2013年

これは、Reactive Extensionsの一部です。
Mark

答えではありません。これは質問にまったく答えません。
makhdumi

5

これは私がスレッドセーフの境界ブロックキューのopに来たものです。

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;
    }
}

このクラスをインスタンス化する方法など、このライブラリを使用していくつかのスレッド関数をキューに入れる方法のコードサンプルを教えてください。
theJerm 2013年

この質問/回答は少し時代遅れです。キューのサポートをブロックするには、System.Collections.Concurrent名前空間を確認する必要があります。
ケビン

2

私はTPLを完全には調査していませんが、ニーズに合ったもの、または少なくともインスピレーションを得るためのリフレクターフードがあるかもしれません。

お役に立てば幸いです。


これは古いことは承知していますが、私のコメントはSOの初心者向けです。OPはすでにこのことを知っています。これは答えではありません、これはコメントであるべきでした。
John Demetriou

0

まあ、あなたはSystem.Threading.Semaphoreクラスを見るかもしれません。それ以外-いいえ、これは自分で作成する必要があります。私の知る限り、そのような組み込みのコレクションはありません。


リソースにアクセスしているスレッドの数を調整するためにそれを見ましたが、ある条件(Collection.Countなど)に基づいてリソースへのすべてのアクセスをブロックすることはできません。AFAIKとにかく
エリックSchoonover

さて、あなたは今と同じように、その部分を自分で行います。単にMaxSizeと_FullEventの代わりにセマフォがあり、コンストラクタで正しい数で初期化します。次に、追加/削除のたびに、WaitForOne()またはRelease()を呼び出します。
Vilx- 2009

それはあなたが今持っているものと大差ありません。ただもっとシンプルな私見。
Vilx- 2009

この動作を示す例を教えていただけますか?このシナリオで必要なSemaphorのサイズを動的に調整する方法はわかりませんでした。キューがいっぱいの場合にのみ、すべてのリソースをブロックできる必要があるためです。
Eric Sc​​hoonover、

ああ、サイズ変更!なぜすぐにそう言わなかったのですか?OK、セマフォはあなたのためではありません。このアプローチで頑張ってください!
Vilx- 2009

-1

最大のスループットが必要で、複数のリーダーが読み取り、1つのライターのみが書き込みを許可する場合、BCLには、コードのスリム化に役立つReaderWriterLockSlimと呼ばれるものがあります...


ただし、キューがいっぱいの場合は誰も書き込みできないようにします。
Eric Sc​​hoonover、

だからあなたはそれを錠と組み合わせる。ここにいくつかの非常に良い例があります albahari.com/threading/part2.aspx#_ProducerConsumerQWaitHandle albahari.com/threading/part4.aspx
DavidN

3
キュー/デキューを使用すると、全員がライターになります...排他ロックはおそらくより実用的です
マークグラベル

これは古いことは承知していますが、私のコメントはSOの初心者向けです。OPはすでにこのことを知っています。これは答えではありません、これはコメントであるべきでした。
John Demetriou
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.