나는 ConcurrentQueue
마지막 N 개의 객체 (일종의 역사)를 보유하는 공유 데이터 구조를 사용 하고 있습니다.
브라우저가 있고 마지막으로 검색된 URL 100 개를 갖고 싶다고 가정합니다. 용량이 가득 차면 새 항목 삽입 (대기열에 넣기)시 가장 오래된 (첫 번째) 항목을 자동으로 삭제 (대기열에서 빼기)하는 대기열을 원합니다 (기록에서 100 개 주소).
어떻게 사용 System.Collections
합니까?
나는 ConcurrentQueue
마지막 N 개의 객체 (일종의 역사)를 보유하는 공유 데이터 구조를 사용 하고 있습니다.
브라우저가 있고 마지막으로 검색된 URL 100 개를 갖고 싶다고 가정합니다. 용량이 가득 차면 새 항목 삽입 (대기열에 넣기)시 가장 오래된 (첫 번째) 항목을 자동으로 삭제 (대기열에서 빼기)하는 대기열을 원합니다 (기록에서 100 개 주소).
어떻게 사용 System.Collections
합니까?
답변:
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)) ;
}
}
}
q
은 (는) lock
다른 스레드가 동시에 액세스하는 것을 방지 하도록 개체에 대해 비공개 입니다.
Count
및 TryDequeue
BCL 동시에 의해 서로 연관되지 않도록주의 두 개의 독립적 인 작업이다.
ConcurrentQueue<T>
A의 객체를 Queue<T>
더 경량 객체입니다.
Enqueue
는 여전히 원래 큐를 호출합니다. 즉,이 답변은 수락 된 것으로 표시되지만 완전히 완전히 깨졌습니다.
나는 약간의 변형을 위해 갈 것입니다 ... FixedSizeQueue에서 Linq 확장을 사용할 수 있도록 ConcurrentQueue를 확장하십시오.
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);
}
}
}
}
유용하다고 생각하는 사람을 위해 위의 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);
}
}
}
그 가치를 위해 여기에 안전하고 안전하지 않은 사용으로 표시된 몇 가지 메서드가 포함 된 경량 순환 버퍼가 있습니다.
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
는 잠금 내부의 메서드를 호출 합니다.약간 장황하지만 스레드로부터 안전해야하는 메서드의 잠금 외부에서 안전하지 않은 메서드를 호출하는 것과 같은 명백한 오류가 발생합니다.
고정 크기 대기열에 대한 설명입니다.
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();
}
}
재미를 위해 여기에 댓글 작성자의 우려 사항을 대부분 해결한다고 생각하는 또 다른 구현이 있습니다. 특히 스레드 안전성은 잠금없이 달성되며 구현은 래핑 클래스에 의해 숨겨집니다.
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();
}
}
_queue.Enqueue(obj)
하지만 전에 Interlocked.Increment(ref _count)
, 다른 스레드 호출 .Count
? 잘못된 카운트를 얻을 것입니다. 나는 다른 문제를 확인하지 않았습니다.
내 버전은 일반 버전의 하위 클래스 일뿐 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;
}
}
답을 하나 더 추가하겠습니다. 왜 이것이 다른 사람보다?
1) 단순성. 크기를 보장하려는 시도는 훌륭하고 좋지만 자체 문제를 나타낼 수있는 불필요한 복잡성으로 이어집니다.
2) IReadOnlyCollection을 구현합니다. 즉, Linq를 사용하고 IEnumerable을 기대하는 다양한 항목에 전달할 수 있습니다.
3) 잠금 없음. 위의 많은 솔루션은 잠금이없는 컬렉션에서는 잘못된 잠금을 사용합니다.
4) BlockingCollection과 함께 컬렉션을 사용하려는 경우 중요한 IProducerConsumerCollection을 포함하여 ConcurrentQueue와 동일한 메서드, 속성 및 인터페이스 집합을 구현합니다.
이 구현은 TryDequeue가 실패 할 경우 예상보다 더 많은 항목으로 끝날 수 있지만 발생 빈도는 필연적으로 성능을 저하시키고 자체 예상치 못한 문제를 유발하는 특수 코드의 가치가 없어 보입니다.
절대적으로 크기를 보장하고 싶다면 Prune () 또는 유사한 메서드를 구현하는 것이 가장 좋은 생각 인 것 같습니다. 다른 메서드 (TryDequeue 포함)에서 ReaderWriterLockSlim 읽기 잠금을 사용하고 정리할 때만 쓰기 잠금을 사용할 수 있습니다.
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();
}
코딩의 즐거움을 위해 ' 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);
}
위의 솔루션 중 일부가 다중 스레드 환경에서 사용될 때 크기를 초과 할 수 있음을 알아 차린 용도에 따라 다릅니다. 어쨌든 내 사용 사례는 마지막 5 개의 이벤트를 표시하는 것이었고 이벤트를 대기열에 쓰는 여러 스레드와 여기에서 읽고 Winform 컨트롤에 표시하는 다른 스레드가 있습니다. 그래서 이것이 제 해결책이었습니다.
편집 : 구현 내에서 이미 잠금을 사용하고 있으므로 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
.
아직 아무도 말하지 않았기 때문에 .. 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라는 것입니다. 그러나 필요한 경우 재정의 할 수 있습니다.
허용되는 대답은 피할 수있는 부작용이있을 것입니다.
아래 링크는 아래에 예제를 작성할 때 사용한 참고 자료입니다.
Microsoft의 문서는 잠금을 사용하기 때문에 약간 오해의 소지가 있지만 segement 클래스를 잠급니다. 세그먼트 클래스 자체는 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;
}
}
}
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);
}
이것은 내 대기열 버전입니다.
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을 사용하는 것이 유용하다는 것을 알게되었습니다. 밑받침 모음이 변경되면 오류가 발생합니다.
이중 카운트 검사는 일부 상황에서 잠금을 방지하는 것입니다.