Черга з фіксованим розміром, яка автоматично видає старі значення за новими запитами


121

Я використовую ConcurrentQueueдля спільної структури даних, метою якої є зберігання останніх N об'єктів, переданих до неї (вид історії).

Припустимо, у нас є браузер і ми хочемо мати останні 100 переглянутих URL-адрес. Я хочу, щоб черга, яка автоматично відкидає (відмальовує) найстаріший (перший) запис після нової вставки (enqueue), коли ємність буде повна (100 адрес в історії).

Як я можу це зробити System.Collections?



Це не було призначено спеціально для вас, але для тих, хто стикається з цим питанням і може вважати його корисним. btw, він теж говорить про C #. Чи вдалося вам прочитати всі відповіді (за 2 хвилини) і зрозуміти, що там немає коду C #? У всякому разі, я не впевнений у собі, а отже, це коментар ...

Можна просто загорнути методи в замок. Враховуючи, що вони швидкі, ви можете просто заблокувати весь масив. Це, мабуть, дупа. Пошук реалізацій кругового буфера з кодом C # може вам щось знайти. У будь-якому випадку, удачі.

Відповіді:


111

Я б написав клас обгортки, який на Enqueue перевірив би кількість, а потім видалити, коли кількість перевищить ліміт.

 public class FixedSizedQueue<T>
 {
     ConcurrentQueue<T> q = new ConcurrentQueue<T>();
     private object lockObject = new object();

     public int Limit { get; set; }
     public void Enqueue(T obj)
     {
        q.Enqueue(obj);
        lock (lockObject)
        {
           T overflow;
           while (q.Count > Limit && q.TryDequeue(out overflow)) ;
        }
     }
 }

4
qє приватним для об'єкта, так що lockзастереження запобігає одночасному доступу інших потоків.
Річард Шнайдер

14
Це не дуже гарна ідея. Вся мета паралельних колекцій BCL полягає в тому, щоб забезпечити безкоштовну паралельну безпеку з міркувань продуктивності. Блокування у вашому коді компрометує цю користь. Насправді я не бачу причини, що вам потрібно заблокувати дек.
KFL

2
@KFL, потрібно заблокувати , тому що Countі TryDequeueдві незалежні операції, відхід не синхронізується з BCL Concurrent.
Річард Шнайдер

9
@RichardSchneider Якщо вам потрібно самостійно вирішувати проблеми одночасності, тоді було б непогано обміняти ConcurrentQueue<T>об'єкт на Queue<T>більш легкий об’єкт.
0b101010

6
Не визначайте власну чергу, просто використовуйте успадковану. Якщо ви робите так само, як і раніше, ви не можете нічого більше робити зі значеннями черги, усі інші функції, окрім вашої нової Enqueue, все ще буде викликати початкову чергу. Іншими словами, хоча ця відповідь позначена як прийнята, вона повністю і повністю порушена.
Габор

104

Я б подумав про незначний варіант ... розширити ConcurrentQueue, щоб мати можливість використовувати розширення Linq на FixedSizeQueue

public class FixedSizedQueue<T> : ConcurrentQueue<T>
{
    private readonly object syncObject = new object();

    public int Size { get; private set; }

    public FixedSizedQueue(int size)
    {
        Size = size;
    }

    public new void Enqueue(T obj)
    {
        base.Enqueue(obj);
        lock (syncObject)
        {
            while (base.Count > Size)
            {
                T outObj;
                base.TryDequeue(out outObj);
            }
        }
    }
}

1
що відбувається, коли хтось статично знає екземпляр як ConcurrentQueue <T>, він просто обійшов ваше "нове" ключове слово.
mhand

6
@mhand Якщо хтось хотів це зробити; тоді вони вирішили використовувати для початку об'єкт ConcurrentQueue <T> ... Це спеціальний клас зберігання. Ніхто не прагне, щоб це було подано до .NET-бази. Ви прагнули створити проблему заради неї.
Дейв Лоуренс

9
моя думка, а не підкласифікація, можливо, вам слід просто загорнути чергу ... це нав'язує бажану поведінку у всіх випадках. Крім того, оскільки це власний клас зберігання, давайте зробимо це повністю на замовлення, лише розкрийте необхідні нам операції, підкласифікація - це неправильний інструмент ІМХО.
mhand

3
@mhand Так, я розумію, що ви говорите .. Я міг би обернути чергу і виставити нумератор черги, щоб використати розширення Linq.
Дейв Лоуренс

1
Я згоден з @mhand, ви не повинні успадковувати ConcurrentQueue, оскільки метод Enqueue не є віртуальним. Вам слід проксі-сервіс і за бажанням реалізувати весь інтерфейс.
Кріс Марісіч

29

Для всіх, хто вважає це корисним, ось деякий робочий код, заснований на відповіді Річарда Шнайдера вище:

public class FixedSizedQueue<T>
{
    readonly ConcurrentQueue<T> queue = new ConcurrentQueue<T>();

    public int Size { get; private set; }

    public FixedSizedQueue(int size)
    {
        Size = size;
    }

    public void Enqueue(T obj)
    {
        queue.Enqueue(obj);

        while (queue.Count > Size)
        {
            T outObj;
            queue.TryDequeue(out outObj);
        }
    }
}

1
Відмова від вказаних причин (блокування при використанні ConcurrentQueue погано), крім того, що не реалізується жоден із необхідних інтерфейсів, щоб це була справжня колекція.
Джош

11

Для чого варто, ось легкий круговий буфер з деякими методами, позначеними для безпечного та небезпечного використання.

public class CircularBuffer<T> : IEnumerable<T>
{
    readonly int size;
    readonly object locker;

    int count;
    int head;
    int rear;
    T[] values;

    public CircularBuffer(int max)
    {
        this.size = max;
        locker = new object();
        count = 0;
        head = 0;
        rear = 0;
        values = new T[size];
    }

    static int Incr(int index, int size)
    {
        return (index + 1) % size;
    }

    private void UnsafeEnsureQueueNotEmpty()
    {
        if (count == 0)
            throw new Exception("Empty queue");
    }

    public int Size { get { return size; } }
    public object SyncRoot { get { return locker; } }

    #region Count

    public int Count { get { return UnsafeCount; } }
    public int SafeCount { get { lock (locker) { return UnsafeCount; } } }
    public int UnsafeCount { get { return count; } }

    #endregion

    #region Enqueue

    public void Enqueue(T obj)
    {
        UnsafeEnqueue(obj);
    }

    public void SafeEnqueue(T obj)
    {
        lock (locker) { UnsafeEnqueue(obj); }
    }

    public void UnsafeEnqueue(T obj)
    {
        values[rear] = obj;

        if (Count == Size)
            head = Incr(head, Size);
        rear = Incr(rear, Size);
        count = Math.Min(count + 1, Size);
    }

    #endregion

    #region Dequeue

    public T Dequeue()
    {
        return UnsafeDequeue();
    }

    public T SafeDequeue()
    {
        lock (locker) { return UnsafeDequeue(); }
    }

    public T UnsafeDequeue()
    {
        UnsafeEnsureQueueNotEmpty();

        T res = values[head];
        values[head] = default(T);
        head = Incr(head, Size);
        count--;

        return res;
    }

    #endregion

    #region Peek

    public T Peek()
    {
        return UnsafePeek();
    }

    public T SafePeek()
    {
        lock (locker) { return UnsafePeek(); }
    }

    public T UnsafePeek()
    {
        UnsafeEnsureQueueNotEmpty();

        return values[head];
    }

    #endregion


    #region GetEnumerator

    public IEnumerator<T> GetEnumerator()
    {
        return UnsafeGetEnumerator();
    }

    public IEnumerator<T> SafeGetEnumerator()
    {
        lock (locker)
        {
            List<T> res = new List<T>(count);
            var enumerator = UnsafeGetEnumerator();
            while (enumerator.MoveNext())
                res.Add(enumerator.Current);
            return res.GetEnumerator();
        }
    }

    public IEnumerator<T> UnsafeGetEnumerator()
    {
        int index = head;
        for (int i = 0; i < count; i++)
        {
            yield return values[index];
            index = Incr(index, size);
        }
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }

    #endregion
}

Мені подобається використовувати Foo()/SafeFoo()/UnsafeFoo()конвенцію:

  • FooМетоди виклику UnsafeFooяк за замовчуванням.
  • UnsafeFoo методи вільно змінюють стан без блокування, вони повинні викликати лише інші небезпечні методи.
  • SafeFooметоди виклику UnsafeFooметодів всередині замка.

Це трохи детально, але це робить очевидні помилки, як, наприклад, виклик небезпечних методів поза замком у способі, який повинен бути безпечним для потоків, більш очевидним.


5

Ось моя черга з фіксованим розміром

Він використовує звичайну чергу, щоб уникнути накладних витрат синхронізації, коли використовується Countвластивість ConcurrentQueue. Він також реалізує IReadOnlyCollectionтак, щоб можна було використовувати методи LINQ. Решта дуже схожа на інші відповіді тут.

[Serializable]
[DebuggerDisplay("Count = {" + nameof(Count) + "}, Limit = {" + nameof(Limit) + "}")]
public class FixedSizedQueue<T> : IReadOnlyCollection<T>
{
    private readonly Queue<T> _queue = new Queue<T>();
    private readonly object _lock = new object();

    public int Count { get { lock (_lock) { return _queue.Count; } } }
    public int Limit { get; }

    public FixedSizedQueue(int limit)
    {
        if (limit < 1)
            throw new ArgumentOutOfRangeException(nameof(limit));

        Limit = limit;
    }

    public FixedSizedQueue(IEnumerable<T> collection)
    {
        if (collection is null || !collection.Any())
           throw new ArgumentException("Can not initialize the Queue with a null or empty collection", nameof(collection));

        _queue = new Queue<T>(collection);
        Limit = _queue.Count;
    }

    public void Enqueue(T obj)
    {
        lock (_lock)
        {
            _queue.Enqueue(obj);

            while (_queue.Count > Limit)
                _queue.Dequeue();
        }
    }

    public void Clear()
    {
        lock (_lock)
            _queue.Clear();
    }

    public IEnumerator<T> GetEnumerator()
    {
        lock (_lock)
            return new List<T>(_queue).GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

3

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

public class FixedSizeQueue<T> : IReadOnlyCollection<T>
{
  private ConcurrentQueue<T> _queue = new ConcurrentQueue<T>();
  private int _count;

  public int Limit { get; private set; }

  public FixedSizeQueue(int limit)
  {
    this.Limit = limit;
  }

  public void Enqueue(T obj)
  {
    _queue.Enqueue(obj);
    Interlocked.Increment(ref _count);

    // Calculate the number of items to be removed by this thread in a thread safe manner
    int currentCount;
    int finalCount;
    do
    {
      currentCount = _count;
      finalCount = Math.Min(currentCount, this.Limit);
    } while (currentCount != 
      Interlocked.CompareExchange(ref _count, finalCount, currentCount));

    T overflow;
    while (currentCount > finalCount && _queue.TryDequeue(out overflow))
      currentCount--;
  }

  public int Count
  {
    get { return _count; }
  }

  public IEnumerator<T> GetEnumerator()
  {
    return _queue.GetEnumerator();
  }

  System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
  {
    return _queue.GetEnumerator();
  }
}

1
Це порушується при одночасному застосуванні - що робити, якщо нитку попередньо викликують після виклику, _queue.Enqueue(obj)але раніше Interlocked.Increment(ref _count), а інший потік дзвонить .Count? Це отримає неправильний підрахунок. Я не перевірив інших питань.
KFL

3

Моя версія - це просто підклас звичайних Queue. Нічого особливого, окрім того, як бачити всіх учасників, і все одно йдеться з назвою теми, яку я міг би також поставити тут. Він також повертає звільнених людей на всякий випадок.

public sealed class SizedQueue<T> : Queue<T>
{
    public int FixedCapacity { get; }
    public SizedQueue(int fixedCapacity)
    {
        this.FixedCapacity = fixedCapacity;
    }

    /// <summary>
    /// If the total number of item exceed the capacity, the oldest ones automatically dequeues.
    /// </summary>
    /// <returns>The dequeued value, if any.</returns>
    public new T Enqueue(T item)
    {
        base.Enqueue(item);
        if (base.Count > FixedCapacity)
        {
            return base.Dequeue();
        }
        return default;
    }
}

2

Додамо ще одну відповідь. Чому це над іншими?

1) Простота. Намагання гарантувати розмір - це добре і добре, але призводить до зайвої складності, яка може виявляти власні проблеми.

2) Реалізує IReadOnlyCollection, тобто ви можете використовувати Linq на ньому та передавати їх у різноманітні речі, які очікують на IEnumerable.

3) Без блокування. У багатьох рішеннях, описаних вище, використовуються замки, що невірно в безблочній колекції.

4) Реалізує той самий набір методів, властивостей та інтерфейсів, які робить ConcurrentQueue, включаючи IProducerConsumerCollection, що важливо, якщо ви хочете використовувати колекцію з BlockingCollection.

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

Якщо ви абсолютно хочете гарантувати розмір, реалізація методу Prune () або подібного методу здається найкращою ідеєю. Ви можете використовувати блокування читання ReaderWriterLockSlim в інших методах (включаючи TryDequeue) та брати блокування запису лише під час обрізки.

class ConcurrentFixedSizeQueue<T> : IProducerConsumerCollection<T>, IReadOnlyCollection<T>, ICollection {
    readonly ConcurrentQueue<T> m_concurrentQueue;
    readonly int m_maxSize;

    public int Count => m_concurrentQueue.Count;
    public bool IsEmpty => m_concurrentQueue.IsEmpty;

    public ConcurrentFixedSizeQueue (int maxSize) : this(Array.Empty<T>(), maxSize) { }

    public ConcurrentFixedSizeQueue (IEnumerable<T> initialCollection, int maxSize) {
        if (initialCollection == null) {
            throw new ArgumentNullException(nameof(initialCollection));
        }

        m_concurrentQueue = new ConcurrentQueue<T>(initialCollection);
        m_maxSize = maxSize;
    }

    public void Enqueue (T item) {
        m_concurrentQueue.Enqueue(item);

        if (m_concurrentQueue.Count > m_maxSize) {
            T result;
            m_concurrentQueue.TryDequeue(out result);
        }
    }

    public void TryPeek (out T result) => m_concurrentQueue.TryPeek(out result);
    public bool TryDequeue (out T result) => m_concurrentQueue.TryDequeue(out result);

    public void CopyTo (T[] array, int index) => m_concurrentQueue.CopyTo(array, index);
    public T[] ToArray () => m_concurrentQueue.ToArray();

    public IEnumerator<T> GetEnumerator () => m_concurrentQueue.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator () => GetEnumerator();

    // Explicit ICollection implementations.
    void ICollection.CopyTo (Array array, int index) => ((ICollection)m_concurrentQueue).CopyTo(array, index);
    object ICollection.SyncRoot => ((ICollection) m_concurrentQueue).SyncRoot;
    bool ICollection.IsSynchronized => ((ICollection) m_concurrentQueue).IsSynchronized;

    // Explicit IProducerConsumerCollection<T> implementations.
    bool IProducerConsumerCollection<T>.TryAdd (T item) => ((IProducerConsumerCollection<T>) m_concurrentQueue).TryAdd(item);
    bool IProducerConsumerCollection<T>.TryTake (out T item) => ((IProducerConsumerCollection<T>) m_concurrentQueue).TryTake(out item);

    public override int GetHashCode () => m_concurrentQueue.GetHashCode();
    public override bool Equals (object obj) => m_concurrentQueue.Equals(obj);
    public override string ToString () => m_concurrentQueue.ToString();
}

2

Просто тому, що ще ніхто цього не сказав. Ви можете використовувати a LinkedList<T>та додати безпеку потоку:

public class Buffer<T> : LinkedList<T>
{
    private int capacity;

    public Buffer(int capacity)
    {
        this.capacity = capacity;   
    }

    public void Enqueue(T item)
    {
        // todo: add synchronization mechanism
        if (Count == capacity) RemoveLast();
        AddFirst(item);
    }

    public T Dequeue()
    {
        // todo: add synchronization mechanism
        var last = Last.Value;
        RemoveLast();
        return last;
    }
}

Варто зазначити, що порядок перерахування за замовчуванням буде LIFO у цьому прикладі. Але це може бути відмінено за потреби.


1

Для вашого задоволення від кодування я передаю вам ' ConcurrentDeck'

public class ConcurrentDeck<T>
{
   private readonly int _size;
   private readonly T[] _buffer;
   private int _position = 0;

   public ConcurrentDeck(int size)
   {
       _size = size;
       _buffer = new T[size];
   }

   public void Push(T item)
   {
       lock (this)
       {
           _buffer[_position] = item;
           _position++;
           if (_position == _size) _position = 0;
       }
   }

   public T[] ReadDeck()
   {
       lock (this)
       {
           return _buffer.Skip(_position).Union(_buffer.Take(_position)).ToArray();
       }
   }
}

Приклад використання:

void Main()
{
    var deck = new ConcurrentDeck<Tuple<string,DateTime>>(25);
    var handle = new ManualResetEventSlim();
    var task1 = Task.Factory.StartNew(()=>{
    var timer = new System.Timers.Timer();
    timer.Elapsed += (s,a) => {deck.Push(new Tuple<string,DateTime>("task1",DateTime.Now));};
    timer.Interval = System.TimeSpan.FromSeconds(1).TotalMilliseconds;
    timer.Enabled = true;
    handle.Wait();
    }); 
    var task2 = Task.Factory.StartNew(()=>{
    var timer = new System.Timers.Timer();
    timer.Elapsed += (s,a) => {deck.Push(new Tuple<string,DateTime>("task2",DateTime.Now));};
    timer.Interval = System.TimeSpan.FromSeconds(.5).TotalMilliseconds;
    timer.Enabled = true;
    handle.Wait();
    }); 
    var task3 = Task.Factory.StartNew(()=>{
    var timer = new System.Timers.Timer();
    timer.Elapsed += (s,a) => {deck.Push(new Tuple<string,DateTime>("task3",DateTime.Now));};
    timer.Interval = System.TimeSpan.FromSeconds(.25).TotalMilliseconds;
    timer.Enabled = true;
    handle.Wait();
    }); 
    System.Threading.Thread.Sleep(TimeSpan.FromSeconds(10));
    handle.Set();
    var outputtime = DateTime.Now;
    deck.ReadDeck().Select(d => new {Message = d.Item1, MilliDiff = (outputtime - d.Item2).TotalMilliseconds}).Dump(true);
}

1
Мені подобається ця реалізація, але зауважте, що коли її не додано, вона повертається за замовчуванням (T)
Daniel Leach

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

1

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

EDIT: Оскільки ми вже використовуємо блокування в рамках нашої реалізації, нам не потрібна ConcurrentQueue, але це може підвищити продуктивність.

class FixedSizedConcurrentQueue<T> 
{
    readonly Queue<T> queue = new Queue<T>();
    readonly object syncObject = new object();

    public int MaxSize { get; private set; }

    public FixedSizedConcurrentQueue(int maxSize)
    {
        MaxSize = maxSize;
    }

    public void Enqueue(T obj)
    {
        lock (syncObject)
        {
            queue.Enqueue(obj);
            while (queue.Count > MaxSize)
            {
                queue.Dequeue();
            }
        }
    }

    public T[] ToArray()
    {
        T[] result = null;
        lock (syncObject)
        {
            result = queue.ToArray();
        }

        return result;
    }

    public void Clear()
    {
        lock (syncObject)
        {
            queue.Clear();
        }
    }
}

EDIT: Нам дійсно не потрібно syncObjectв наведеному вище прикладі, і ми можемо скоріше використовувати queueоб’єкт, оскільки ми не реініціалізуємо queueжодну функцію і її позначено як readonlyінакше.


0

Прийнята відповідь матиме побічні ефекти, які можна уникнути.

Дрібнозернисті механізми замикання та замикання

Посилання нижче - це посилання, які я використовував, коли писав свій приклад нижче.

Хоча документація від Microsoft трохи вводить в оману, оскільки вони використовують блокування, проте вони фіксують класи сегментації. Класи сегментів самі використовують Interlocked.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;

namespace Lib.Core
{
    // Sources: 
    // https://docs.microsoft.com/en-us/dotnet/standard/collections/thread-safe/
    // https://docs.microsoft.com/en-us/dotnet/api/system.threading.interlocked?view=netcore-3.1
    // https://github.com/dotnet/runtime/blob/master/src/libraries/System.Private.CoreLib/src/System/Collections/Concurrent/ConcurrentQueue.cs
    // https://github.com/dotnet/runtime/blob/master/src/libraries/System.Private.CoreLib/src/System/Collections/Concurrent/ConcurrentQueueSegment.cs

    /// <summary>
    /// Concurrent safe circular buffer that will used a fixed capacity specified and resuse slots as it goes.
    /// </summary>
    /// <typeparam name="TObject">The object that you want to go into the slots.</typeparam>
    public class ConcurrentCircularBuffer<TObject>
    {
        private readonly ConcurrentQueue<TObject> _queue;

        public int Capacity { get; private set; }

        public ConcurrentCircularBuffer(int capacity)
        {
            if(capacity <= 0)
            {
                throw new ArgumentException($"The capacity specified '{capacity}' is not valid.", nameof(capacity));
            }

            // Setup the queue to the initial capacity using List's underlying implementation.
            _queue = new ConcurrentQueue<TObject>(new List<TObject>(capacity));

            Capacity = capacity;
        }

        public void Enqueue(TObject @object)
        {
            // Enforce the capacity first so the head can be used instead of the entire segment (slow).
            while (_queue.Count + 1 > Capacity)
            {
                if (!_queue.TryDequeue(out _))
                {
                    // Handle error condition however you want to ie throw, return validation object, etc.
                    var ex = new Exception("Concurrent Dequeue operation failed.");
                    ex.Data.Add("EnqueueObject", @object);
                    throw ex;
                }
            }

            // Place the item into the queue
            _queue.Enqueue(@object);
        }

        public TObject Dequeue()
        {
            if(_queue.TryDequeue(out var result))
            {
                return result;
            }

            return default;
        }
    }
}

0

Ось ще одна реалізація, яка максимально використовує базовий ConcurrentQueue, надаючи ті самі інтерфейси, доступні через ConcurrentQueue.

/// <summary>
/// This is a FIFO concurrent queue that will remove the oldest added items when a given limit is reached.
/// </summary>
/// <typeparam name="TValue"></typeparam>
public class FixedSizedConcurrentQueue<TValue> : IProducerConsumerCollection<TValue>, IReadOnlyCollection<TValue>
{
    private readonly ConcurrentQueue<TValue> _queue;

    private readonly object _syncObject = new object();

    public int LimitSize { get; }

    public FixedSizedConcurrentQueue(int limit)
    {
        _queue = new ConcurrentQueue<TValue>();
        LimitSize = limit;
    }

    public FixedSizedConcurrentQueue(int limit, System.Collections.Generic.IEnumerable<TValue> collection)
    {
        _queue = new ConcurrentQueue<TValue>(collection);
        LimitSize = limit;

    }

    public int Count => _queue.Count;

    bool ICollection.IsSynchronized => ((ICollection) _queue).IsSynchronized;

    object ICollection.SyncRoot => ((ICollection)_queue).SyncRoot; 

    public bool IsEmpty => _queue.IsEmpty;

    // Not supported until .NET Standard 2.1
    //public void Clear() => _queue.Clear();

    public void CopyTo(TValue[] array, int index) => _queue.CopyTo(array, index);

    void ICollection.CopyTo(Array array, int index) => ((ICollection)_queue).CopyTo(array, index);

    public void Enqueue(TValue obj)
    {
        _queue.Enqueue(obj);
        lock( _syncObject )
        {
            while( _queue.Count > LimitSize ) {
                _queue.TryDequeue(out _);
            }
        }
    }

    public IEnumerator<TValue> GetEnumerator() => _queue.GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable<TValue>)this).GetEnumerator();

    public TValue[] ToArray() => _queue.ToArray();

    public bool TryAdd(TValue item)
    {
        Enqueue(item);
        return true;
    }

    bool IProducerConsumerCollection<TValue>.TryTake(out TValue item) => TryDequeue(out item);

    public bool TryDequeue(out TValue result) => _queue.TryDequeue(out result);

    public bool TryPeek(out TValue result) => _queue.TryPeek(out result);

}

-1

Це моя версія черги:

public class FixedSizedQueue<T> {
  private object LOCK = new object();
  ConcurrentQueue<T> queue;

  public int MaxSize { get; set; }

  public FixedSizedQueue(int maxSize, IEnumerable<T> items = null) {
     this.MaxSize = maxSize;
     if (items == null) {
        queue = new ConcurrentQueue<T>();
     }
     else {
        queue = new ConcurrentQueue<T>(items);
        EnsureLimitConstraint();
     }
  }

  public void Enqueue(T obj) {
     queue.Enqueue(obj);
     EnsureLimitConstraint();
  }

  private void EnsureLimitConstraint() {
     if (queue.Count > MaxSize) {
        lock (LOCK) {
           T overflow;
           while (queue.Count > MaxSize) {
              queue.TryDequeue(out overflow);
           }
        }
     }
  }


  /// <summary>
  /// returns the current snapshot of the queue
  /// </summary>
  /// <returns></returns>
  public T[] GetSnapshot() {
     return queue.ToArray();
  }
}

Мені здається корисним конструктор, побудований на IEnumerable, і мені здається корисним отримати GetSnapshot, щоб у момент виклику був багатопотоковий безпечний список (масив у цьому випадку) елементів, який не піднімається помилки, якщо змінюється нижня колекція.

Подвійна перевірка підрахунку полягає у запобіганні блокування в деяких обставинах.


1
Голосування за блокування в черзі. Якщо ви абсолютно хочете заблокувати, найкраще буде ReaderWriterLockSlim (якщо припускати, що ви будете брати блокування читання частіше, ніж блокування запису). GetSnapshot також не потрібен. Якщо ви реалізуєте IReadOnlyCollection <T> (що вам слід для IEnumerable семантики), ToList () буде виконувати ту ж функцію.
Джош

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