Створення черги блокування <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
.Невідомо, як вбудовані класи допоможуть у цьому сценарії. Більшість наведених тут відповідей є застарілими. Найновіші відповіді дивіться внизу. Перегляньте колекції, захищені від потоку. Відповіді можуть бути застарілими, але це все-таки хороше питання!
Том А

Я думаю, що це все-таки хороша ідея дізнатися про Monitor.Wait / Pulse / PulseAll, навіть якщо у нас є нові одночасні класи у .NET.
thewpfguy

1
Погодьтеся з @thewpfguy. Вам захочеться зрозуміти основні механізми блокування за кадром. Також варто відзначити, що System.Collections.Concurrent існував до квітня 2010 року, а потім лише у Visual Studio 2010 і вище. Однозначно не варіант для проведення аукціонів VS2008 ...
Вік

Якщо ви читаєте це зараз, погляньте на System.Threading.Channels для мульти-записувача / мульти-читачів, обмежених, необов'язково блокуючих реалізацію цього для .NET Core та .NET Standard.
Марк

Відповіді:


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 - якщо встановлено, порожня черга просто повертається (а не блокується):

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 в конструктор черги <T>. Ви можете додати ще одного конструктора до свого класу, щоб вмістити його.
РічардОД

3
Чому SizeQueue, чому не FixedSizeQueue?
mindless.panda

4
@Lasse - він звільняє блокування під час Wait, тому інші потоки можуть придбати його. Він повертає замок (и), коли прокидається.
Марк Гравелл

1
Приємно, як я вже сказав, я щось не отримував :) Це впевнено змушує мене переглянути частину мого потокового коду ....
Lasse V. Karlsen


14

"Як це можна покращити?"

Ну, вам потрібно переглянути кожен метод вашого класу і подумати, що буде, якби інший потік одночасно викликав цей метод чи будь-який інший метод. Наприклад, ви ставите замок у методі Remove, але не у методі Add. Що станеться, якщо один потік додається одночасно з іншим потоком видалення? Погані речі.

Також врахуйте, що метод може повернути другий об'єкт, який забезпечує доступ до внутрішніх даних першого об'єкта - наприклад, GetEnumerator. Уявіть, що один потік проходить через цей нумератор, інший - одночасно змінює список. Не добре.

Добре правило полягає в тому, щоб зробити це більш простим, як правильно, скоротивши кількість методів у класі до абсолютного мінімуму.

Зокрема, не успадковуйте інший клас контейнерів, оскільки ви відкриєте всі методи цього класу, надаючи спосіб абоненту пошкодити внутрішні дані або побачити частково повні зміни даних (так само погано, оскільки дані здається зіпсованим в цей момент). Сховайте всі деталі та будьте абсолютно нещадні щодо того, як ви дозволяєте їм отримати доступ.

Я б настійно радив вам скористатись нестандартними рішеннями - отримати книгу про нарізання різьби або використовувати сторонні бібліотеки. Інакше, враховуючи те, що ви намагаєтесь, ви довго будете налагоджувати код.

Крім того, чи не було б більше сенсу для Remove повертати елемент (скажімо, той, який був доданий першим, оскільки це черга), а не той, хто викликає, хто вибирає конкретний елемент? І коли черга порожня, можливо, Remove також слід заблокувати.

Оновлення: Відповідь Марка реально реалізує всі ці пропозиції! :) Але я залишу це тут, оскільки це може бути корисно зрозуміти, чому його версія - це таке вдосконалення.


12

Ви можете використовувати BlockingCollection і ConcurrentQueue в System.Collections.Concurrent Іменний простір

 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 черга. Отже, я не думаю, що це потрібно.
Кертіс Уайт

Чи зберігає BlockingCollection впорядкування, як чергу?
joelc

Так, коли вона ініціалізована з ConcurrentQueue
Андреас

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

Не обов’язково цілком безпечний, але дуже простий.


Що таке Тема <t>? У мене немає жодної роздільної здатності для простору імен.
theJerm

Це частина Реактивних розширень.
Марк Rendle

Не відповідь. Це зовсім не відповідає на питання.
махдумі

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 простір імен для блокування підтримки черги.
Кевін

2

Я не до кінця вивчив TPL, але у них може бути щось, що відповідає вашим потребам, або, принаймні, якийсь кормовий відбивач, щоб вирвати деякий натхнення.

Сподіваюся, що це допомагає.


Я усвідомлюю, що це старе, але мій коментар - для новачків ЗО, оскільки ОП це вже знає сьогодні. Це не відповідь, це мав бути коментар.
Джон Деметріу

0

Ну, ви можете подивитися на System.Threading.Semaphoreзаняття. Крім цього - ні, ви повинні зробити це самостійно. AFAIK немає такої вбудованої колекції.


Я звернув увагу на те, щоб зменшити кількість потоків, які мають доступ до ресурсу, але це не дозволяє вам заблокувати весь доступ до ресурсу на основі певної умови (наприклад, Collection.Count). AFAIK так чи інакше
Ерік Шкоунвер

Що ж, ти робиш свою частину так само, як зараз. Просто замість MaxSize та _FullEvent у вас є Semaphore, який ви ініціалізуєте з правильним підрахунком у конструкторі. Потім після кожного додавання / видалення ви телефонуєте WaitForOne () або Release ().
Vilx-

Це не сильно відрізняється від того, що у вас зараз. Просто більш простий IMHO.
Vilx-

Чи можете ви навести мені приклад, що показує, що це працює? Я не бачив, як динамічно регулювати розмір Semaphor, який вимагає цей сценарій. Оскільки вам доведеться блокувати всі ресурси, лише якщо черга заповнена.
Ерік Шконовер

Ах, міняючи розмір! Чому ти не сказав так негайно? Гаразд, значить, семафор не для вас. Успіхів у такому підході!
Vilx-

-1

Якщо ви хочете отримати максимальну пропускну спроможність, дозволяючи читачам читати і писати лише один автор, BCL має щось, що називається ReaderWriterLockSlim, що повинно допомогти зменшити код ...


Я хочу, щоб ніхто не міг написати, якщо черга повна.
Ерік Шконовер

Так ви поєднуєте його з замком. Ось деякі дуже хороші приклади albahari.com/threading/part2.aspx#_ProducerConsumerQWaitHandle albahari.com/threading/part4.aspx
DavidN

3
З чергою / декею всі письменники ... ексклюзивний замок, можливо, буде більш прагматичним
Марк Гравелл

Я усвідомлюю, що це старе, але мій коментар - для новачків ЗО, оскільки ОП це вже знає сьогодні. Це не відповідь, це мав бути коментар.
Джон Деметріу
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.