.NET에서 Producer/Consumer를 구현하는 기초 인터페이스 - IProducerConsumerCollection<T>
IProducerConsumerCollection<T>은,
IProducerConsumerCollection<T> Interface
; https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent.iproducerconsumercollection-1
public interface IProducerConsumerCollection<T> : IEnumerable<T>, IEnumerable, ICollection
{
void CopyTo(T[] array, int index);
T[] ToArray();
bool TryAdd(T item);
bool TryTake([MaybeNullWhen(false)] out T item);
}
단순히 생각하면 기존의 ICollection 구현에서 (대표적으로) TryAdd, TryTake와 CopyTo, ToArray 메서드를 추가한 유형입니다. 즉, 이름과 어울릴법한 Producer/Consumer를 위한 특정 메서드가 추가된 것은 아닙니다. 얼핏 인터페이스의 이름만 봐서는 TryAdd/TryTake 메서드를 어떤 식으로 구현해야 할지 감도 잘 안 옵니다.
사실, 엄밀히 말하면 IProducerConsumerCollection<T>보다는 IConcurrentCollection<T>가 더 어울릴 것입니다. 또는 좀 더 직접적으로 말하면 IThreadSafeCollection도 나쁘지 않은 이름입니다. 아마도 그랬다면 TryAdd나 TryTake(를 비롯해 기타 ICollection 구현) 메서드가 thread-safe해야 한다는 것을 단번에 알아챌 수 있었을 것입니다.
물론, 대개의 경우 Producer/Consumer 구현은 스레드에 걸쳐 사용된다는 점에서 그나마 IProducerConsumerCollection의 정당성이 부여됩니다. 가령 A 스레드가 데이터를 생성하고, B 스레드에서 그 데이터를 소비하는 식이므로, 이로 인해 Thread-safe한 성격을 가진 데이터 구조가 필요하고, 그중에서도 Collection인 경우 특별히 IProducerConsumerCollection<T> 인터페이스를 구현하도록 의도한 것입니다.
실제로 "
IProducerConsumerCollection<T> Interface" 문서의 예제 코드를 봐도,
public class SafeStack<T> : IProducerConsumerCollection<T>
{
private readonly object m_lockObject = new object();
private readonly Stack<T> m_sequentialStack;
public SafeStack()
{
m_sequentialStack = new Stack<T>();
}
public SafeStack(IEnumerable<T> collection)
{
m_sequentialStack = new Stack<T>(collection);
}
public void Push(T item)
{
lock (m_lockObject) m_sequentialStack.Push(item);
}
public bool TryPop(out T? item)
{
bool rval = true;
lock (m_lockObject)
{
if (m_sequentialStack.Count == 0) { item = default(T); rval = false; }
else
{
item = m_sequentialStack.Pop();
}
}
return rval;
}
public bool TryTake([MaybeNullWhen(false)] out T item)
{
return TryPop(out item);
}
public bool TryAdd(T item)
{
Push(item);
return true; // Push doesn't fail
}
public T[] ToArray()
{
lock (m_lockObject)
{
return m_sequentialStack.ToArray();
}
}
public void CopyTo(T[] array, int index)
{
lock (m_lockObject) m_sequentialStack.CopyTo(array, index);
}
public IEnumerator<T> GetEnumerator()
{
lock (m_lockObject)
{
return new Stack<T>(m_sequentialStack).GetEnumerator();
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable<T>)this).GetEnumerator();
}
public bool IsSynchronized
{
get { return true; }
}
public object SyncRoot
{
get { return m_lockObject; }
}
public int Count
{
get { return m_sequentialStack.Count; }
}
public void CopyTo(Array array, int index)
{
lock (m_lockObject) ((ICollection)m_sequentialStack).CopyTo(array, index);
}
}
단순히 기존의 Stack<T> 자료구조에 thread-safe 기능만 얹어 SafeStack<T>를 만들어 IProducerConsumerCollection<T> 인터페이스를 적용하고 있습니다. 또한, 닷넷 BCL의 System.Collections.Concurrent 네임스페이스에서 제공하는 컬렉션도 모두,
System.Collections.Concurrent.ConcurrentBag<T>
System.Collections.Concurrent.ConcurrentQueue<T>
System.Collections.Concurrent.ConcurrentStack<T>
IProducerConsumerCollection<T>을 상속받고 있습니다.
아마 여러분들도 그동안 컬렉션 중에 thread-safe가 필요해서 동기화를 시킨 컬렉션들이 있을 것입니다. 그것들 모두가 잠재적으로는 IProducerConsumerCollection<T> 인터페이스를 상속받아도 무방한 것입니다.
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]