การสร้างคิวการบล็อค <T> ใน. NET?


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 มีคลาสบิวด์อินเพื่อช่วยในสถานการณ์นี้อย่างไร คำตอบส่วนใหญ่ที่แสดงไว้ที่นี่ล้าสมัย ดูคำตอบล่าสุดที่ด้านล่าง ดูที่คอลเลกชันการปิดกั้นที่ปลอดภัยต่อเธรด คำตอบอาจล้าสมัย แต่ก็ยังเป็นคำถามที่ดี!
ทอม

ฉันคิดว่ามันยังเป็นความคิดที่ดีที่จะเรียนรู้เกี่ยวกับ Monitor.Wait / Pulse / PulseAll แม้ว่าเราจะมีคลาสที่เกิดขึ้นพร้อมกันใหม่ใน. NET
thewpfguy

1
เห็นด้วยกับ @thewpfguy คุณจะต้องเข้าใจกลไกการล็อคพื้นฐานที่อยู่เบื้องหลัง นอกจากนี้ยังมีข้อสังเกตว่า Systems.Collections.Concurrent ไม่มีอยู่จนถึงเดือนเมษายน 2010 และเฉพาะใน Visual Studio 2010 ขึ้นไป แตกหักไม่ได้ตัวเลือกสำหรับ VS2008 ลึกหนาบางถือ ...
วิก

หากคุณกำลังอ่านสิ่งนี้อยู่ตอนนี้ให้ดูที่ System.Threading.Channels สำหรับการใช้งานตัวเขียนแบบหลายตัวเขียน / ตัวอ่านหลายตัวที่มีขอบเขต จำกัด และปิดกั้นทางเลือกนี้สำหรับ. NET Core และ. NET Standard
Mark Rendle

คำตอบ:


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 และผ่านใน WaitHandle ยุติในการก่อสร้าง ...
แซม Saffron

1
@ Marc- การเพิ่มประสิทธิภาพหากคุณคาดหวังว่าคิวจะเข้าถึงความจุได้ตลอดเวลาคือการส่งค่า maxSize ไปยังตัวสร้างของ Queue <T> คุณสามารถเพิ่มคอนสตรัคเตอร์อื่นในชั้นเรียนของคุณเพื่อรองรับสิ่งนั้น
RichardOD

3
ทำไมต้อง SizeQueue ทำไมไม่แก้ไข SizeQueue
mindless.panda

4
@ Lasse - มันปล่อยล็อค (s) ในระหว่างWaitเพื่อให้หัวข้ออื่น ๆ สามารถได้รับมัน มันจะคืนค่าล็อคเมื่อมันตื่นขึ้น
Marc Gravell

1
ดีที่ผมกล่าวว่ามีบางอย่างที่ผมไม่ได้รับ :) ที่แน่ใจว่าทำให้ผมอยากจะทบทวนบางส่วนของด้ายรหัสของฉัน ....
Lasse V. Karlsen

59

ใช้. net 4 BlockingCollection เพื่อจัดคิวการใช้ Add () เพื่อ dequeue use Take () มันใช้ภายในไม่ใช่การปิดกั้น ConcurrentQueue ข้อมูลเพิ่มเติมที่นี่รวดเร็วและดีที่สุดเทคนิคคิวผู้ผลิต / ผู้บริโภค BlockingCollection เทียบกับคิวที่เกิดขึ้นพร้อมกัน


14

"สิ่งนี้จะปรับปรุงได้อย่างไร"

คุณต้องดูทุกวิธีในชั้นเรียนของคุณและพิจารณาว่าจะเกิดอะไรขึ้นถ้าเธรดอื่นเรียกวิธีนั้นหรือวิธีอื่นพร้อมกัน ตัวอย่างเช่นคุณใส่การล็อคในวิธีการลบ แต่ไม่ได้อยู่ในวิธีการเพิ่ม จะเกิดอะไรขึ้นถ้าหนึ่งเธรดเพิ่มพร้อมกันกับเธรดอื่นเอาออก สิ่งที่ไม่ดี

นอกจากนี้ให้พิจารณาว่าวิธีการสามารถส่งคืนวัตถุที่สองที่ให้การเข้าถึงข้อมูลภายในของวัตถุแรก - ตัวอย่างเช่น GetEnumerator ลองนึกภาพหนึ่งเธรดจะผ่านตัวแจงนับเธรดอื่นกำลังแก้ไขรายการในเวลาเดียวกัน ไม่ดี.

กฎง่ายๆคือการทำให้สิ่งนี้ง่ายขึ้นเพื่อให้ถูกต้องโดยการลดจำนวนวิธีในชั้นเรียนให้เหลือน้อยที่สุด

อย่าสืบทอดคลาสคอนเทนเนอร์อื่นเนื่องจากคุณจะเปิดเผยวิธีการทั้งหมดของชั้นเรียนโดยให้วิธีการสำหรับผู้เรียกเสียหายข้อมูลภายในหรือเพื่อดูการเปลี่ยนแปลงที่สมบูรณ์บางส่วนของข้อมูล (เช่นเดียวกับที่ไม่ดีเพราะข้อมูล ดูเหมือนจะเสียหายในขณะนั้น) ซ่อนรายละเอียดทั้งหมดและระมัดระวังเกี่ยวกับวิธีที่คุณอนุญาตให้เข้าถึงได้

ฉันขอแนะนำให้คุณใช้วิธีแก้ปัญหานอกชั้นวาง - รับหนังสือเกี่ยวกับเกลียวหรือใช้ห้องสมุดบุคคลที่สาม มิฉะนั้นหากคุณพยายามสิ่งใดคุณจะต้องแก้ไขข้อบกพร่องของโค้ดเป็นเวลานาน

นอกจากนี้จะไม่เหมาะสมกว่าหรือที่จะลบเพื่อส่งคืนรายการ (เช่นรายการที่ถูกเพิ่มก่อนเนื่องจากเป็นคิว) แทนที่จะเป็นผู้โทรเลือกรายการที่ต้องการหรือไม่ และเมื่อคิวว่างเปล่าบางที Remove ควรบล็อกด้วย

Update: คำตอบของ Marc ใช้คำแนะนำเหล่านี้จริง ๆ ! :) แต่ฉันจะปล่อยไว้ที่นี่เพราะมันอาจจะเป็นประโยชน์ที่จะเข้าใจว่าทำไมรุ่นของเขาถึงได้รับการปรับปรุง


12

คุณสามารถใช้BlockingCollectionและConcurrentQueueใน System.Collections.Concurrent Namespace

 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

ฉันเพิ่งทำเรื่องนี้โดยใช้ส่วนขยายปฏิกิริยาและจดจำคำถามนี้:

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

ไม่จำเป็นต้องปลอดภัยอย่างสิ้นเชิง แต่ง่ายมาก


Subject <t> คืออะไร ฉันไม่มีตัวแก้ไขสำหรับเนมสเปซของมัน
theJerm

มันเป็นส่วนหนึ่งของการขยายปฏิกิริยา
Mark Rendle

ไม่ใช่คำตอบ นี่ไม่ได้ตอบคำถามเลย
makhdumi

5

นี่คือสิ่งที่ฉันเข้ามาสำหรับเธรดการบล็อกคิวที่ปลอดภัย

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

คำถาม / คำตอบนี้เก่าไปนิด คุณควรดู System.Collections.Concurrent namespace เพื่อบล็อกการสนับสนุนคิว
Kevin

2

ฉันยังไม่ได้สำรวจTPLอย่างเต็มที่แต่พวกเขาอาจมีบางสิ่งที่เหมาะกับความต้องการของคุณหรืออย่างน้อยที่สุดสัตว์เลื้อยคลาน Reflector บางตัวจะขัดขวางแรงบันดาลใจบางอย่างจาก

หวังว่าจะช่วย


ฉันรู้ว่านี่เป็นรุ่นเก่า แต่ความคิดเห็นของฉันมีไว้สำหรับผู้ที่เพิ่งมาถึง SO เนื่องจาก OP ทราบแล้วในวันนี้ นี่ไม่ใช่คำตอบนี่ควรเป็นความคิดเห็น
John Demetriou

0

คุณอาจดูSystem.Threading.Semaphoreชั้นเรียน นอกเหนือจากนั้น - ไม่คุณต้องทำสิ่งนี้ด้วยตัวเอง AFAIK ไม่มีคอลเลกชันในตัวดังกล่าว


ฉันดูที่การควบคุมปริมาณของเธรดที่เข้าถึงทรัพยากร แต่ไม่อนุญาตให้คุณบล็อกการเข้าถึงทรัพยากรทั้งหมดตามเงื่อนไขบางอย่าง (เช่น Collection.Count) AFAIK ต่อไป
Eric Schoonover

คุณทำส่วนนั้นด้วยตัวเองเหมือนที่คุณทำตอนนี้ เพียงแค่แทน MaxSize และ _FullEvent คุณมีเซมาฟอร์ซึ่งคุณเริ่มต้นด้วยการนับที่ถูกต้องในตัวสร้าง จากนั้นทุกครั้งที่ Add / Remove คุณโทรไปที่ WaitForOne () หรือ Release ()
Vilx-

มันไม่แตกต่างไปจากที่คุณมีในตอนนี้ IMHO ง่าย ๆ
Vilx-

คุณสามารถยกตัวอย่างที่แสดงการทำงานนี้ให้ฉันได้ไหม ฉันไม่เห็นวิธีการปรับขนาดของ Semaphor แบบไดนามิกซึ่งสถานการณ์นี้ต้องการ เนื่องจากคุณจะต้องสามารถปิดกั้นทรัพยากรทั้งหมดเฉพาะเมื่อคิวเต็ม
Eric Schoonover

อ่าขนาดที่เปลี่ยนไป! ทำไมคุณไม่พูดทันที ตกลงแล้วสัญญาณไม่ได้สำหรับคุณ ขอให้โชคดีกับวิธีนี้!
Vilx-

-1

หากคุณต้องการปริมาณงานสูงสุดให้ผู้อ่านหลายคนอ่านและมีเพียงหนึ่งนักเขียนเท่านั้นที่จะเขียน BCL มีสิ่งที่เรียกว่า ReaderWriterLockSlim ที่จะช่วยลดรหัสของคุณ ...


ฉันไม่ต้องการให้ใครเขียนถ้าคิวเต็ม
Eric Schoonover

ดังนั้นคุณรวมกับล็อค นี่คือตัวอย่างที่ดีมาก albahari.com/threading/part2.aspx#_ProducerConsumerQWaitHandle albahari.com/threading/part4.aspx
DavidN

3
ด้วยคิว / dequeue ทุกคนเป็นนักเขียน ... การล็อคแบบเอกสิทธิ์อาจจะใช้งานได้จริงมากกว่านี้
Marc Gravell

ฉันรู้ว่านี่เป็นรุ่นเก่า แต่ความคิดเห็นของฉันมีไว้สำหรับผู้ที่เพิ่งมาถึง SO เนื่องจาก OP ทราบแล้วในวันนี้ นี่ไม่ใช่คำตอบนี่ควรเป็นความคิดเห็น
John Demetriou
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.