Tạo hàng đợi chặn <T> trong .NET?


163

Tôi có một kịch bản trong đó tôi có nhiều luồng thêm vào hàng đợi và nhiều luồng đọc từ cùng một hàng. Nếu hàng đợi đạt đến một kích thước cụ thể, tất cả các luồng đang lấp đầy hàng đợi sẽ bị chặn khi thêm cho đến khi một mục bị xóa khỏi hàng đợi.

Giải pháp dưới đây là những gì tôi đang sử dụng ngay bây giờ và câu hỏi của tôi là: Làm thế nào để cải thiện điều này? Có một đối tượng đã kích hoạt hành vi này trong BCL mà tôi nên sử dụng không?

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 làm thế nào có các lớp dựng sẵn để trợ giúp với kịch bản này. Hầu hết các câu trả lời được liệt kê ở đây là lỗi thời. Xem các câu trả lời gần đây nhất ở phía dưới. Nhìn vào bộ sưu tập chặn luồng an toàn. Các câu trả lời có thể bị lỗi thời, nhưng nó vẫn là một câu hỏi hay!
Tom A

Tôi nghĩ rằng vẫn nên tìm hiểu về Monitor.Wait / Pulse / PulseAll ngay cả khi chúng tôi có các lớp đồng thời mới trong .NET.
thewpfguy

1
Đồng ý với @thewpfguy. Bạn sẽ muốn hiểu các cơ chế khóa cơ bản đằng sau hậu trường. Cũng đáng chú ý rằng Systems.Collections.Conc hiện không tồn tại cho đến tháng 4 năm 2010 và sau đó chỉ trong Visual Studio 2010 trở lên. Chắc chắn không phải là một lựa chọn cho việc giữ VS2008 ...
Vic

Nếu bạn đang đọc cái này ngay bây giờ, hãy xem System.Threading.Channels để biết cách triển khai đa trình đọc / đa người đọc, giới hạn, chặn tùy chọn này cho .NET Core và .NET Standard.
Đánh dấu

Câu trả lời:


200

Điều đó có vẻ rất không an toàn (rất ít đồng bộ hóa); làm thế nào về một cái gì đó như:

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

(biên tập)

Trong thực tế, bạn muốn có một cách để đóng hàng đợi để người đọc bắt đầu thoát ra một cách sạch sẽ - có lẽ giống như một cờ bool - nếu được đặt, một hàng đợi trống chỉ trả về (thay vì chặn):

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
Làm thế nào về việc thay đổi sự chờ đợi thành WaitAny và chuyển qua một waithandle chấm dứt khi xây dựng ...
Sam Saffron

1
@ Marc- một tối ưu hóa, nếu bạn mong muốn hàng đợi luôn đạt công suất, sẽ chuyển giá trị maxSize vào hàm tạo của Hàng đợi <T>. Bạn có thể thêm một hàm tạo khác vào lớp của bạn để chứa điều đó.
RichardOD

3
Tại sao SizeQueue, tại sao không FixedSizeQueue?
mindless.panda

4
@Lasse - nó giải phóng (các) khóa trong khi Waitcác chủ đề khác có thể có được nó. Nó lấy lại khóa (khi) thức dậy.
Marc Gravell

1
Thật tuyệt, như tôi đã nói, có một cái gì đó tôi đã không nhận được :) Điều đó chắc chắn khiến tôi muốn xem lại một số mã chủ đề của mình ....
Lasse V. Karlsen


14

"Làm thế nào điều này có thể được cải thiện?"

Chà, bạn cần xem xét mọi phương thức trong lớp và xem xét điều gì sẽ xảy ra nếu một luồng khác đồng thời gọi phương thức đó hoặc bất kỳ phương thức nào khác. Ví dụ: bạn đặt một khóa trong phương thức Xóa, nhưng không phải trong phương thức Thêm. Điều gì xảy ra nếu một luồng Thêm cùng lúc với một luồng khác Loại bỏ? Những điều tồi tệ.

Cũng xem xét rằng một phương thức có thể trả về một đối tượng thứ hai cung cấp quyền truy cập vào dữ liệu nội bộ của đối tượng thứ nhất - ví dụ: GetEnumerator. Hãy tưởng tượng một luồng đang đi qua điều tra viên đó, một luồng khác đang sửa đổi danh sách cùng một lúc. Không tốt.

Một nguyên tắc tốt là làm cho điều này trở nên đơn giản hơn bằng cách cắt giảm số lượng phương thức trong lớp đến mức tối thiểu.

Cụ thể, không kế thừa một lớp container khác, bởi vì bạn sẽ phơi bày tất cả các phương thức của lớp đó, cung cấp một cách để người gọi làm hỏng dữ liệu nội bộ hoặc để xem các thay đổi hoàn toàn một phần đối với dữ liệu (cũng tệ như vậy, vì dữ liệu xuất hiện hỏng tại thời điểm đó). Ẩn tất cả các chi tiết và hoàn toàn tàn nhẫn về cách bạn cho phép truy cập vào chúng.

Tôi thực sự khuyên bạn nên sử dụng các giải pháp sẵn có - hãy lấy một cuốn sách về phân luồng hoặc sử dụng thư viện của bên thứ 3. Mặt khác, với những gì bạn đang cố gắng, bạn sẽ gỡ lỗi mã của mình trong một thời gian dài.

Ngoài ra, sẽ không có ý nghĩa hơn khi Xóa để trả lại một mặt hàng (giả sử, mặt hàng được thêm trước, dưới dạng hàng đợi), thay vì người gọi chọn một mặt hàng cụ thể? Và khi hàng đợi trống, có lẽ Remove cũng nên chặn.

Cập nhật: Câu trả lời của Marc thực sự thực hiện tất cả những gợi ý này! :) Nhưng tôi sẽ để nó ở đây vì nó có thể hữu ích để hiểu tại sao phiên bản của anh ấy là một cải tiến như vậy.


12

Bạn có thể sử dụng BlockingCollectionConcảnQueue trong System.Collections.Conciverse 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 mặc định thành Hàng đợi. Vì vậy, tôi không nghĩ rằng điều này là cần thiết.
Curtis White

BlockingCollection có giữ trật tự như một hàng đợi không?
tham gia

Có, khi nó được khởi tạo với Đồng thờiQueue
Andreas

6

Tôi vừa đánh bật nó bằng phần mở rộng Reactive và nhớ câu hỏi này:

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

Không nhất thiết phải hoàn toàn an toàn, nhưng rất đơn giản.


Chủ đề <t> là gì? Tôi không có bất kỳ trình giải quyết nào cho không gian tên của nó.
theJerm

Đó là một phần của Phần mở rộng phản ứng.
Mark Rendle

Không phải là một câu trả lời. Điều này không trả lời câu hỏi nào cả.
makhdumi

5

Đây là những gì tôi đến op cho một hàng đợi chặn an toàn chủ đề.

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

Bạn có thể cung cấp một số mẫu mã về cách tôi xếp hàng một số hàm luồng bằng thư viện này, bao gồm cả cách tôi khởi tạo lớp này không?
theJerm

Câu hỏi / trả lời này là một chút ngày. Bạn nên nhìn vào không gian tên System.Collections.Conc hiện để chặn hỗ trợ hàng đợi.
Kevin

2

Tôi chưa khám phá đầy đủ TPL nhưng họ có thể có thứ gì đó phù hợp với nhu cầu của bạn, hoặc ít nhất, một số thức ăn gia súc Reflector để lấy cảm hứng từ đó.

Mong rằng sẽ giúp.


Tôi biết rằng điều này đã cũ, nhưng nhận xét của tôi là dành cho những người mới tham gia SO, vì OP đã biết điều này ngày hôm nay. Đây không phải là một câu trả lời, đây nên là một bình luận.
John Demetriou

0

Vâng, bạn có thể nhìn vào System.Threading.Semaphorelớp học. Khác với điều đó - không, bạn phải tự làm điều này. AFAIK không có bộ sưu tập tích hợp như vậy.


Tôi đã xem xét điều đó để điều chỉnh số lượng luồng đang truy cập tài nguyên nhưng nó không cho phép bạn chặn tất cả quyền truy cập vào tài nguyên dựa trên một số điều kiện (như Collection.Count). Dù sao thì AFAIK
Eric Schoonover

Vâng, bạn làm phần đó của bạn, giống như bạn làm bây giờ. Đơn giản thay vì MaxSize và _FullEvent, bạn có Semaphore, bạn khởi tạo với số đếm đúng trong hàm tạo. Sau đó, cứ sau mỗi Thêm / Xóa, bạn gọi WaitForOne () hoặc Release ().
Vilx-

Nó không khác nhiều so với những gì bạn có bây giờ. IMHO đơn giản hơn.
Vilx-

Bạn có thể cho tôi một ví dụ cho thấy điều này làm việc? Tôi không thấy cách điều chỉnh kích thước của Semaphor một cách linh hoạt mà kịch bản này yêu cầu. Vì bạn phải có thể chặn tất cả các tài nguyên chỉ khi hàng đợi đầy.
Eric Schoonover

Ahh, thay đổi kích thước! Tại sao bạn không nói như vậy ngay lập tức? OK, sau đó một semaphore không dành cho bạn. Chúc may mắn với phương pháp này!
Vilx-

-1

Nếu bạn muốn thông lượng tối đa, cho phép nhiều người đọc đọc và chỉ có một người viết để viết, BCL có một thứ gọi là ReaderWriterLockSlim sẽ giúp làm giảm mã của bạn ...


Tôi muốn không ai có thể viết nếu hàng đợi đầy.
Eric Schoonover

Vì vậy, bạn kết hợp nó với một khóa. Dưới đây là một số ví dụ rất hay albahari.com/threading/part2.aspx#_ ProducterConsumerQWaitHandle albahari.com/threading/part4.aspx
DavidN

3
Với hàng đợi / dequeue, mọi người đều là nhà văn ... một khóa độc quyền có lẽ sẽ thực dụng hơn
Marc Gravell

Tôi biết rằng điều này đã cũ, nhưng nhận xét của tôi là dành cho những người mới tham gia SO, vì OP đã biết điều này ngày hôm nay. Đây không phải là một câu trả lời, đây nên là một bình luận.
John Demetriou
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.