.NET'te <T> engelleme kuyruğu oluşturuluyor mu?


163

Ben bir kuyruk ve birden çok iş parçacığı aynı kuyruktan okuma birden çok iş parçacığı var bir senaryo var. Sıra belirli bir boyuta ulaştığında ise bütün ipler bir öğe kuyruğundan çıkarılana kadar kuyruğu dolduruyor eklenti üzerinde engellenecektir.

Aşağıdaki çözüm şu anda kullandığım ve sorum şu: Bu nasıl geliştirilebilir? Kullanmam gereken BCL'de bu davranışı etkinleştiren bir nesne var mı?

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 bu senaryoda size yardımcı olacak yerleşik sınıflara sahiptir. Burada listelenen cevapların çoğu eski. En son cevapları en altta görün. İplik korumalı engelleme koleksiyonlarına bakın. Cevaplar eski olabilir, ama yine de iyi bir soru!
Tom A

NET'te yeni eşzamanlı sınıflarımız olsa bile Monitor.Wait / Pulse / PulseAll hakkında bilgi edinmek hala iyi bir fikir.
thewpfguy

1
@Thewpfguy ile aynı fikirde. Perde arkasındaki temel kilitleme mekanizmalarını kavramak isteyeceksiniz. Ayrıca Systems.Collections.Concurrent'ın Nisan 2010'a kadar ve daha sonra yalnızca Visual Studio 2010 ve sonraki sürümlerde mevcut olmadığını da belirtmek gerekir. VS2008 için bir seçenek kesinlikle değil ...
Vic

Bunu şimdi okuyorsanız, .NET Core ve .NET Standard için bunun çok yazarlı / çok okuyuculu, sınırlı, isteğe bağlı olarak engellenen bir uygulaması için System.Threading.Channels'a bakın.
Mark Rendle

Yanıtlar:


200

Bu çok güvensiz görünüyor (çok az senkronizasyon); nasıl bir şey hakkında:

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

(Düzenle)

Gerçekte, kuyruğu kapatmanın bir yolu istersiniz, böylece okuyucular temiz bir şekilde çıkmaya başlarlar - belki de bool bayrağı gibi bir şey - ayarlanmışsa, boş bir kuyruk sadece (engellemek yerine) geri döner:

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
Bir WaitAny için beklemeyi değiştirme ve inşaatta son bir bekleme kolundan geçme ...
Sam Saffron

1
@ Marc- bir optimizasyon, kuyruğun her zaman kapasiteye ulaşmasını bekliyorsanız, maxSize değerini <T> Kuyruğunun yapıcısına iletmek olurdu. Bunun için sınıfınıza başka bir kurucu ekleyebilirsiniz.
RichardOD

3
Neden SizeQueue, neden FixedSizeQueue olmasın?
mindless.panda

4
@Lasse - sırasında Waitdiğer kilitleri elde edebilmek için kilitleri serbest bırakır . Uyandığında kilitleri geri alır.
Marc Gravell

1
Güzel, dediğim gibi, ben alamadım bir şey oldu :) Emin benim iplik kod bazılarını tekrar istiyorum istiyorum ....
Lasse V. Karlsen


14

"Bu nasıl geliştirilebilir?"

Sınıfınızdaki her yönteme bakmanız ve başka bir iş parçacığı aynı anda bu yöntemi veya başka bir yöntemi çağırdığında ne olacağını düşünmeniz gerekir. Örneğin, Remove yöntemine bir kilit koyarsınız, ancak Add yöntemine eklemezsiniz. Bir iş parçacığı başka bir iş parçacığıyla aynı anda Eklenirse Kaldırılırsa ne olur? Kötü şeyler.

Ayrıca, bir yöntemin, ilk nesnenin dahili verilerine (örneğin GetEnumerator) erişim sağlayan ikinci bir nesne döndürebileceğini düşünün. Bir iş parçacığının o numaralandırıcıdan geçtiğini, başka bir iş parçacığının listeyi aynı anda değiştirdiğini düşünün. İyi değil.

İyi bir kural, sınıftaki yöntem sayısını mutlak minimum seviyeye indirerek bunu elde etmeyi kolaylaştırmaktır.

Özellikle, başka bir kapsayıcı sınıfını miras almayın, çünkü bu sınıfın tüm yöntemlerini açığa çıkaracak, arayanın dahili verileri bozması veya verilerde kısmen tamamlanmış değişiklikleri görmesi için bir yol sunacaksınız ( o anda bozuk görünür). Tüm ayrıntıları gizleyin ve bunlara erişime nasıl izin verdiğiniz konusunda tamamen acımasız olun.

Hazır çözümleri kullanmanızı, iş parçacığı hakkında bir kitap almanızı veya 3. taraf kitaplığını kullanmanızı şiddetle tavsiye ederim. Aksi takdirde, denediğiniz şey göz önüne alındığında, kodunuzda uzun süre hata ayıklayacaksınız.

Ayrıca, Kaldır özelliğinin, arayan kişinin belirli bir öğeyi seçmesinden ziyade, bir öğeyi (örneğin, bir sıra olduğu için ilk eklenen) döndürmesi daha anlamlı olmaz mıydı? Ve kuyruk boş olduğunda, belki Kaldır da engellemelidir.

Güncelleme: Marc'ın cevabı aslında tüm bu önerileri uyguluyor! :) Ama bunu burada bırakacağım çünkü versiyonunun neden böyle bir gelişme olduğunu anlamak yararlı olabilir.


12

BlockingCollection ve ConcurrentQueue'yu System.Collections.Concurrent Adamespace içinde kullanabilirsiniz.

 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 varsayılan olarak Kuyruk'tur. Yani, bunun gerekli olduğunu düşünmüyorum.
Curtis White

BlockingCollection sıralamayı kuyruk gibi koruyor mu?
joelc

Evet, bir ConcurrentQueue ile başlatıldığında
Andreas

6

Ben sadece Reaktif Uzantıları kullanarak çaldı ve bu soruyu hatırladım:

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

Mutlaka güvenli değil, ama çok basit.


Konu <t> nedir? Ad alanı için çözümleyici yok.
theJerm

Reaktif Uzantıların bir parçası.
Mark Rendle

Cevap değil. Bu soruya hiç cevap vermiyor.
makhdumi

5

Bu bir iplik güvenli sınırlı engelleme kuyruğu için op geldi.

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

Bu sınıfı nasıl başlatırım da dahil olmak üzere, bu kitaplığı kullanarak bazı iş parçacığı işlevlerini nasıl sıraya koyacağımın bazı kod örneklerini verebilir misiniz?
theJerm

Bu soru / cevap biraz tarihli. Kuyruk desteğini engellemek için System.Collections.Concurrent ad alanına bakmalısınız.
Kevin

2

TPL'yi tam olarak araştırmadım, ancak ihtiyaçlarınızı karşılayan bir şeyleri olabilir veya en azından biraz ilham almak için bazı Reflektör yemleri olabilir.

Umarım yardımcı olur.


Bunun eski olduğunun farkındayım, ancak OP bugün bunu zaten bildiği için yorumum SO'ya yeni gelenler için. Bu bir cevap değil, bu bir yorum olmalıydı.
John Demetriou

0

System.Threading.SemaphoreSınıfa bakabilirsiniz . Bunun dışında - hayır, bunu kendin yapmak zorundasın. AFAIK böyle bir yerleşik koleksiyon yok.


Bir kaynağa erişen iş parçacığı sayısını azaltmak için baktım ama bazı koşullara (Collection.Count gibi) dayalı bir kaynağa tüm erişimi engellemenize izin vermez. Yine de AFAIK
Eric Schoonover

Bu kısmı kendin yapıyorsun, tıpkı senin gibi. Sadece MaxSize ve _FullEvent yerine yapıcıda doğru sayımla başlattığınız Semafor var. Ardından, her Add / Remove (Kaldır / Kaldır) üzerine WaitForOne () veya Release () çağrılır.
Vilx

Şu an sahip olduğunuzdan çok farklı değil. Daha basit IMHO.
Vilx

Bana bu çalışmayı gösteren bir örnek verebilir misiniz? Bu senaryonun gerektirdiği bir Semaforun boyutunu dinamik olarak nasıl ayarlayacağımı görmedim. Tüm kaynakları ancak sıra dolu olduğunda engelleyebilmeniz gerektiğinden.
Eric Schoonover

Ahh, boyut değişiyor! Neden hemen söylemedin? Tamam, o zaman bir semafor sizin için değil. Bu yaklaşımda iyi şanslar!
Vilx-

-1

Maksimum verim istiyorsanız, birden fazla okuyucunun ve yalnızca bir yazarın yazmasına izin verirseniz, BCL'de kodunuzu azaltmaya yardımcı olacak ReaderWriterLockSlim adlı bir şey vardır ...


Ben kuyruk olsa dolu ise hiçbiri yazmak mümkün istiyorum.
Eric Schoonover

Yani bir kilitle birleştiriyorsun. İşte bazı çok güzel örnekler albahari.com/threading/part2.aspx#_ProducerConsumerQWaitHandle albahari.com/threading/part4.aspx
DavidN

3
Sıra / dequeue ile herkes bir yazar ... özel bir kilit belki de daha pragmatik olacak
Marc Gravell

Bunun eski olduğunun farkındayım, ancak OP bugün bunu zaten bildiği için yorumum SO'ya yeni gelenler için. Bu bir cevap değil, bu bir yorum olmalıydı.
John Demetriou
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.