คิวขนาดคงที่ซึ่ง dequeues ค่าเก่าโดยอัตโนมัติตาม enques ใหม่


121

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

สมมติว่าเรามีเบราว์เซอร์และเราต้องการมี URL ที่เรียกดู 100 รายการล่าสุด ฉันต้องการคิวที่วาง (dequeue) รายการที่เก่าที่สุด (แรก) โดยอัตโนมัติเมื่อมีการแทรกรายการใหม่ (enqueue) เมื่อความจุเต็ม (100 ที่อยู่ในประวัติ)

ฉันจะทำสิ่งนั้นให้สำเร็จได้System.Collectionsอย่างไร?



ไม่ได้มีไว้สำหรับคุณโดยเฉพาะ แต่สำหรับใครก็ตามที่เจอคำถามนี้และอาจพบว่ามีประโยชน์ btw มันพูดถึง C # ด้วย คุณได้อ่านคำตอบทั้งหมด (ใน 2 นาที) และคิดว่าไม่มีรหัส C # อยู่ที่นั่นหรือไม่? อย่างไรก็ตามฉันไม่แน่ใจตัวเองและด้วยเหตุนี้จึงเป็นความคิดเห็น ...

คุณสามารถห่อวิธีการไว้ในล็อคได้ เนื่องจากมีความรวดเร็วคุณสามารถล็อคอาร์เรย์ทั้งหมดได้ นี่อาจเป็นการหลอกลวงแม้ว่า การค้นหาการใช้บัฟเฟอร์แบบวงกลมด้วยรหัส C # อาจพบบางอย่าง ยังไงก็ขอให้โชคดี

คำตอบ:


111

ฉันจะเขียนคลาส wrapper ที่ใน Enqueue จะตรวจสอบ Count จากนั้น Dequeue เมื่อจำนวนเกินขีด จำกัด

 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จะป้องกันไม่ให้เธรดอื่นเข้าถึงพร้อมกัน
Richard Schneider

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

2
@KFL จำเป็นต้องล็อกเนื่องจากCountและTryDequeueเป็นการดำเนินการสองอย่างที่เป็นอิสระซึ่งไม่ได้รับการซิงค์โดย BCL Concurrent
Richard Schneider

9
@RichardSchneider หากคุณต้องการจัดการปัญหาการเกิดพร้อมกันด้วยตัวเองคุณควรเปลี่ยนConcurrentQueue<T>วัตถุเป็นQueue<T>วัตถุที่มีน้ำหนักเบากว่า
0b101010

6
อย่ากำหนดคิวของคุณเองเพียงแค่ใช้คิวที่สืบทอดมา หากคุณทำตามที่คุณทำคุณจะไม่สามารถทำอะไรได้อีกด้วยค่าคิวฟังก์ชันอื่น ๆ ทั้งหมด แต่สิ่งใหม่ของคุณEnqueueจะยังคงเรียกคิวเดิม กล่าวอีกนัยหนึ่งแม้ว่าคำตอบนี้จะถูกทำเครื่องหมายว่ายอมรับ แต่ก็มีข้อผิดพลาดอย่างสิ้นเชิง
Gábor

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 framework คุณพยายามสร้างปัญหาเพื่อประโยชน์ของมัน
Dave Lawrence

9
ประเด็นของฉันคือแทนที่จะเป็นคลาสย่อยบางทีคุณควรตัดคิว ... สิ่งนี้บังคับใช้พฤติกรรมที่ต้องการในทุกกรณี นอกจากนี้เนื่องจากเป็นคลาสพื้นที่จัดเก็บข้อมูลที่กำหนดเองเรามาทำให้มันเป็นแบบกำหนดเองโดยสมบูรณ์เปิดเผยเฉพาะการดำเนินการที่เราต้องการการคลาสย่อยเป็นเครื่องมือที่ไม่ถูกต้องที่นี่ IMHO
มือ

3
@mhand ใช่ฉันเข้าใจว่าคุณกำลังพูดอะไร .. ฉันสามารถตัดคิวและแสดงตัวแจงนับคิวเพื่อใช้ประโยชน์จากส่วนขยาย Linq ได้
Dave Lawrence

1
ฉันเห็นด้วยกับ @mhand คุณไม่ควรสืบทอด ConcurrentQueue เพราะเมธอด Enqueue ไม่ใช่เสมือน คุณควรพร็อกซีคิวและใช้อินเทอร์เฟซทั้งหมดหากต้องการ
Chris Marisic

29

สำหรับใครก็ตามที่พบว่ามีประโยชน์นี่คือรหัสการทำงานบางส่วนตามคำตอบของ Richard Schneider ด้านบน:

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 ไม่ดี) นอกเหนือจากการไม่ใช้อินเทอร์เฟซที่จำเป็นใด ๆ เพื่อให้เป็นคอลเล็กชันที่แท้จริง
Josh

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 เพื่อจัดลำดับความสำคัญของผู้อ่านของคุณ
Josh

1

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

แก้ไข: เนื่องจากเราใช้การล็อกภายในการใช้งานอยู่แล้วเราจึงไม่จำเป็นต้องใช้ 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();
        }
    }
}

แก้ไข: เราไม่จำเป็นต้องใช้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 () จะให้บริการฟังก์ชันเดียวกัน
Josh

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