.NET에서 블로킹 큐 <T>를 작성 하시겠습니까?


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
.Net이 시나리오에 도움이되는 내장 클래스가 있습니다. 여기에 나열된 대부분의 답변은 더 이상 사용되지 않습니다. 하단에서 가장 최근 답변을 참조하십시오. 스레드 안전 차단 모음을 살펴보십시오. 답변이 더 이상 사용되지 않을 수 있지만 여전히 좋은 질문입니다!
Tom A

.NET에 새로운 동시 클래스가 있더라도 Monitor.Wait / Pulse / PulseAll에 대해 배우는 것이 여전히 좋은 생각이라고 생각합니다.
thewpfguy

1
@thewpfguy에 동의하십시오. 배후의 기본 잠금 메커니즘을 이해하고 싶을 것입니다. 또한 Systems.Collections.Concurrent는 2010 년 4 월까지 존재하지 않았으며 Visual Studio 2010 이상에서만 존재했음을 주목할 가치가 있습니다. VS2008 홀드 아웃을위한 옵션은 아닙니다.
Vic

지금 읽고 있다면 .NET Core 및 .NET Standard에 대해 다중 작성기 / 다중 판독기의 경계를 설정하고 선택적으로 차단하는 System.Threading.Channels를 살펴보십시오.
Mark Rendle

답변:


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 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로 변경하고 건설 중 종료 대기 핸들을 전달하는 것은 어떻습니까?
Sam Saffron

1
@ Marc- 큐가 항상 용량에 도달 할 것으로 예상되는 경우 최적화는 maxSize 값을 Queue <T>의 생성자에 전달하는 것입니다. 이를 위해 클래스에 다른 생성자를 추가 할 수 있습니다.
RichardOD

3
SizeQueue를 선택해야하는 이유는 무엇입니까?
mindless.panda

4
@Lasse-동안 잠금을 해제 Wait하므로 다른 스레드가 잠금 을 획득 할 수 있습니다. 깨어날 때 잠금을 되 찾습니다.
Marc Gravell

1
내가 말했듯이 니스 : 내가 얻지 못한 것이 있었다 :) 그래야 내 스레드 코드 중 일부를 다시 방문하고 싶습니다 ....
Lasse V. Karlsen


14

"어떻게 개선 할 수 있습니까?"

글쎄, 클래스의 모든 메소드를 살펴보고 다른 스레드가 해당 메소드 또는 다른 메소드를 동시에 호출하면 어떻게 될지 고려해야합니다. 예를 들어, Add 메서드가 아닌 Remove 메서드에는 잠금을 설정합니다. 한 스레드가 다른 스레드 제거와 동시에 추가되면 어떻게됩니까? 나쁜 것들.

또한 메소드가 첫 번째 오브젝트의 내부 데이터 (예 : GetEnumerator)에 대한 액세스를 제공하는 두 번째 오브젝트를 리턴 할 수 있음을 고려하십시오. 한 스레드가 해당 열거자를 통과하고 다른 스레드가 목록을 동시에 수정한다고 가정하십시오. 안좋다.

경험상 가장 좋은 방법은 클래스의 메소드 수를 절대 최소값으로 줄여서 더 간단하게 만드는 것입니다.

특히 다른 컨테이너 클래스를 상속하지 마십시오. 클래스의 모든 메소드를 노출시켜 호출자가 내부 데이터를 손상 시키거나 데이터가 부분적으로 완전히 변경된 것을 볼 수있는 방법을 제공하기 때문에 해당 시점에 손상된 것으로 표시됨). 모든 세부 사항을 숨기고 액세스를 허용하는 방법에 대해 완전히 무자비하십시오.

기성품 솔루션을 사용하는 것이 좋습니다. 스레딩에 대한 책을 얻거나 타사 라이브러리를 사용하십시오. 그렇지 않으면 시도중인 것을 감안할 때 오랫동안 코드를 디버깅 할 것입니다.

또한 Remove가 호출자가 특정 항목을 선택하는 대신 항목 (예 : 대기열이므로 처음에 추가 된 항목)을 반환하는 것이 더 합리적이지 않습니까? 대기열이 비어 있으면 Remove도 차단해야합니다.

업데이트 : Marc의 답변은 실제로 이러한 모든 제안을 구현합니다! :) 그러나 그의 버전이 왜 그렇게 개선되었는지 이해하는 것이 도움이 될 수 있으므로 여기에 남겨 두겠습니다.


12

System.Collections.Concurrent 네임 스페이스에서 BlockingCollectionConcurrentQueue 를 사용할 수 있습니다.

 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로 초기화 될 때
Andreas

6

방금 Reactive Extensions를 사용 하여이 문제를 해결 했으며이 질문을 기억했습니다.

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

Reactive Extensions의 일부입니다.
마크 렌들

답이 아닙니다. 이것은 전혀 질문에 대답하지 않습니다.
makhdumi

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을 완전히 탐구하지 않았다 하지는 않았지만, 그들은 당신의 요구에 맞는 것이거나 최소한 영감을 얻기 위해 어떤 리플렉터 사료를 가질 수도 있습니다.

희망이 도움이됩니다.


나는 이것이 오래되었다는 것을 알고 있지만 OP는 이미 이것을 알고 있기 때문에 초보자가 그렇게 생각합니다. 이것은 답변이 아닙니다. 이것은 주석이어야합니다.
John Demetriou

0

글쎄, 당신은 System.Threading.Semaphore수업을 볼 수 있습니다 . 그 외에는-아니, 당신은 이것을 직접해야합니다. AFAIK에는 이러한 내장 컬렉션이 없습니다.


리소스에 액세스하는 스레드 수를 제한하는 것을 보았지만 Collection.Count와 같은 특정 조건에 따라 리소스에 대한 모든 액세스를 차단할 수는 없습니다. 어쨌든 AFAIK
Eric Schoonover

글쎄, 당신은 지금 당신처럼 그 부분을 당신의 소프를합니다. MaxSize 및 _FullEvent 대신 Semaphore가 생성자에서 올바른 수로 초기화됩니다. 그런 다음 모든 추가 / 제거시 WaitForOne () 또는 Release ()를 호출합니다.
Vilx- 2018

지금 가지고있는 것과 크게 다르지 않습니다. 더 간단한 IMHO.
Vilx- 2018

이 작업을 보여주는 예를 들어 주시겠습니까? 이 시나리오에 필요한 Semaphor의 크기를 동적으로 조정하는 방법을 보지 못했습니다. 큐가 가득 찬 경우에만 모든 자원을 차단할 수 있어야합니다.
Eric Schoonover

아, 사이즈 변경! 왜 그렇게 말하지 않았어? 그렇다면 세마포어는 당신을위한 것이 아닙니다. 이 방법으로 행운을 빕니다!
Vilx-

-1

최대 처리량을 원하고 여러 독자가 읽고 한 명의 작성자 만 쓸 수 있도록하려면 BCL에는 ReaderWriterLockSlim이라는 코드가있어 코드를 줄이는 데 도움이됩니다.


큐가 가득 찬 경우 아무도 쓸 수 없기를 바랍니다.
Eric Schoonover

그래서 당신은 자물쇠와 결합합니다. 다음은 아주 좋은 예입니다. albahari.com/threading/part2.aspx#_ProducerConsumerQWaitHandle albahari.com/threading/part4.aspx
DavidN

3
큐 / 디큐으로, 모두가 독점 잠금 아마도 더 실용적인 것입니다 ... 작가입니다
마크 Gravell

나는 이것이 오래되었다는 것을 알고 있지만 OP는 이미 이것을 알고 있기 때문에 초보자가 그렇게 생각합니다. 이것은 답변이 아닙니다. 이것은 주석이어야합니다.
John Demetriou
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.