비동기 BlockingCollection <T>과 같은 것이 있습니까?


85

나는 싶습니다 await의 결과에 BlockingCollection<T>.Take()비동기, 그래서 스레드를 차단하지 않습니다. 다음과 같은 것을 찾고 있습니다.

var item = await blockingCollection.TakeAsync();

나는 이것을 할 수 있다는 것을 안다.

var item = await Task.Run(() => blockingCollection.Take());

그러나 그것은 다른 스레드 (의 ThreadPool)가 대신 차단 되기 때문에 전체 아이디어를 죽 입니다.

대안이 있습니까?


2
await Task.Run(() => blockingCollection.Take())작업 을 사용 하면 작업이 다른 스레드에서 수행되고 UI 스레드가 차단되지 않습니다.
Selman Genç 2014 년

8
@ Selman22, 이것은 UI 앱이 아닙니다. 라이브러리 내보내기 Task기반 API입니다. 예를 들어 ASP.NET에서 사용할 수 있습니다. 문제의 코드는 거기에서 잘 확장되지 않을 것입니다.
avo

ConfigureAwait이후에 사용 되었다면 여전히 문제 가 Run()될까요? [ed. 결코 마음, 당신은 지금] 무슨 말을하는지 볼
MojoFilter

답변:


95

내가 아는 네 가지 대안이 있습니다.

첫 번째는 비동기 및 작업 을 지원하는 스레드 세이프 대기열을 제공하는 Channels 입니다. 채널은 고도로 최적화되어 있으며 임계 값에 도달하면 일부 항목을 삭제할 수 있습니다.ReadWrite

다음은 TPL DataflowBufferBlock<T> 에서 가져온 것 입니다. 당신은 단지 하나의 소비자가있는 경우 사용 하거나 , 또는 단지는 링크 . 자세한 내용 은 내 블로그를 참조하십시오 .OutputAvailableAsyncReceiveAsyncActionBlock<T>

마지막 두 가지는 내가 만든 유형이며 AsyncEx 라이브러리 에서 사용할 수 있습니다 .

AsyncCollection<T>async거의 동일 BlockingCollection<T>하며 ConcurrentQueue<T>또는 같은 동시 생산자 / 소비자 컬렉션을 래핑 할 수 ConcurrentBag<T>있습니다. TakeAsync컬렉션의 항목을 비동기 적으로 소비 하는 데 사용할 수 있습니다 . 자세한 내용 은 내 블로그를 참조하십시오 .

AsyncProducerConsumerQueue<T>더 이식 가능하고 async호환되는 생산자 / 소비자 대기열입니다. 를 사용 DequeueAsync하여 큐에서 항목을 비동기 적으로 사용할 수 있습니다 . 자세한 내용 은 내 블로그를 참조하십시오 .

마지막 세 가지 대안은 동기 및 비동기 풋 앤 테이크를 허용합니다.


12
CodePlex가 마침내 종료되는시기에 대한 Git Hub 링크 : github.com/StephenCleary/AsyncEx
Paul

API 문서에 메서드가 포함되어 AsyncCollection.TryTakeAsync있지만 다운로드 한 Nito.AsyncEx.Coordination.dll 5.0.0.0(최신 버전) 에서 찾을 수 없습니다 . 참조 된 Nito.AsyncEx.Concurrent.dll패키지에 없습니다 . 내가 무엇을 놓치고 있습니까?
Theodor Zoulias

@TheodorZoulias :이 메서드는 v5에서 제거되었습니다. v5 API 문서는 여기에 있습니다 .
Stephen Cleary

오 감사. 컬렉션을 열거하는 가장 쉽고 안전한 방법 인 것 같습니다. while ((result = await collection.TryTakeAsync()).Success) { }. 왜 제거 되었습니까?
Theodor Zoulias

1
@TheodorZoulias : 왜냐하면 "시도"는 다른 사람들에게 다른 것을 의미하기 때문입니다. "Try"메서드를 다시 추가 할 생각이지만 실제로는 원래 메서드와 다른 의미를 갖습니다. 또한 향후 버전에서 비동기 스트림을 지원하는 것도 고려 중입니다. 지원되는 경우 가장 좋은 소비 방법이 될 것입니다.
Stephen Cleary

21

... 또는 다음과 같이 할 수 있습니다.

using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

public class AsyncQueue<T>
{
    private readonly SemaphoreSlim _sem;
    private readonly ConcurrentQueue<T> _que;

    public AsyncQueue()
    {
        _sem = new SemaphoreSlim(0);
        _que = new ConcurrentQueue<T>();
    }

    public void Enqueue(T item)
    {
        _que.Enqueue(item);
        _sem.Release();
    }

    public void EnqueueRange(IEnumerable<T> source)
    {
        var n = 0;
        foreach (var item in source)
        {
            _que.Enqueue(item);
            n++;
        }
        _sem.Release(n);
    }

    public async Task<T> DequeueAsync(CancellationToken cancellationToken = default(CancellationToken))
    {
        for (; ; )
        {
            await _sem.WaitAsync(cancellationToken);

            T item;
            if (_que.TryDequeue(out item))
            {
                return item;
            }
        }
    }
}

간단하고 완벽하게 작동하는 비동기식 FIFO 대기열.

참고 : SemaphoreSlim.WaitAsync그 전에 .NET 4.5에 추가되었는데, 이것이 그렇게 간단하지는 않았습니다.


2
무한의 용도는 무엇입니까 for? 세마포어가 해제되면 대기열에 대기열에서 제거 할 항목이 하나 이상 있습니다.
Blendester

2
@Blendester 여러 소비자가 차단 된 경우 경쟁 조건이있을 수 있습니다. 경쟁하는 소비자가 적어도 두 명은 아닌지 확신 할 수 없으며 둘 다 항목을 제거하기 전에 깨어나는지 알 수 없습니다. 레이스의 경우, 데큐에 성공하지 못하면 다시 수면 상태로 돌아가 다른 신호를 기다립니다.
John Leidegren 19

둘 이상의 소비자가 WaitAsync ()를 지나면 대기열에 동일한 수의 항목이 있으므로 항상 성공적으로 대기열에서 제외됩니다. 내가 뭔가를 놓치고 있습니까?
mindcruzer

2
이것은 블로킹 콜렉션이고, 의미는 TryDequeueare, 값으로 반환되거나 전혀 반환되지 않습니다. 기술적으로 독자가 2 명 이상이면 다른 독자가 완전히 깨어나 기 전에 동일한 독자가 두 개 이상의 항목을 소비 할 수 있습니다. 성공 WaitAsync은 소비 할 항목이 대기열에있을 수 있다는 신호일 뿐이며 보장 할 수는 없습니다.
John Leidegren

@JohnLeidegren If the value of the CurrentCount property is zero before this method is called, the method also allows releaseCount threads or tasks blocked by a call to the Wait or WaitAsync method to enter the semaphore.from docs.microsoft.com/en-us/dotnet/api/…WaitAsync 대기열에 항목 이 없으면 어떻게 성공 합니까? N 릴리스가 깨어 난 것보다 N 개 이상의 소비자 semaphore를 깨우면. 그렇지 않습니까?
Ashish Negi

4

다음은 BlockingCollection많은 누락 된 기능과 함께 대기를 지원 하는의 매우 기본적인 구현입니다 . AsyncEnumerable8.0 이전의 C # 버전에 대해 비동기 열거를 가능하게 하는 라이브러리를 사용합니다 .

public class AsyncBlockingCollection<T>
{ // Missing features: cancellation, boundedCapacity, TakeAsync
    private Queue<T> _queue = new Queue<T>();
    private SemaphoreSlim _semaphore = new SemaphoreSlim(0);
    private int _consumersCount = 0;
    private bool _isAddingCompleted;

    public void Add(T item)
    {
        lock (_queue)
        {
            if (_isAddingCompleted) throw new InvalidOperationException();
            _queue.Enqueue(item);
        }
        _semaphore.Release();
    }

    public void CompleteAdding()
    {
        lock (_queue)
        {
            if (_isAddingCompleted) return;
            _isAddingCompleted = true;
            if (_consumersCount > 0) _semaphore.Release(_consumersCount);
        }
    }

    public IAsyncEnumerable<T> GetConsumingEnumerable()
    {
        lock (_queue) _consumersCount++;
        return new AsyncEnumerable<T>(async yield =>
        {
            while (true)
            {
                lock (_queue)
                {
                    if (_queue.Count == 0 && _isAddingCompleted) break;
                }
                await _semaphore.WaitAsync();
                bool hasItem;
                T item = default;
                lock (_queue)
                {
                    hasItem = _queue.Count > 0;
                    if (hasItem) item = _queue.Dequeue();
                }
                if (hasItem) await yield.ReturnAsync(item);
            }
        });
    }
}

사용 예 :

var abc = new AsyncBlockingCollection<int>();
var producer = Task.Run(async () =>
{
    for (int i = 1; i <= 10; i++)
    {
        await Task.Delay(100);
        abc.Add(i);
    }
    abc.CompleteAdding();
});
var consumer = Task.Run(async () =>
{
    await abc.GetConsumingEnumerable().ForEachAsync(async item =>
    {
        await Task.Delay(200);
        await Console.Out.WriteAsync(item + " ");
    });
});
await Task.WhenAll(producer, consumer);

산출:

12 34 5678 9 10


업데이트 : C # 8이 출시되면서 비동기 열거 가 기본 제공 언어 기능이되었습니다. 필수 클래스 ( IAsyncEnumerable, IAsyncEnumerator)는 .NET Core 3.0에 포함되며 .NET Framework 4.6.1+ ( Microsoft.Bcl.AsyncInterfaces ) 용 패키지로 제공됩니다 .

다음은 GetConsumingEnumerable새로운 C # 8 구문을 특징으로 하는 대체 구현입니다.

public async IAsyncEnumerable<T> GetConsumingEnumerable()
{
    lock (_queue) _consumersCount++;
    while (true)
    {
        lock (_queue)
        {
            if (_queue.Count == 0 && _isAddingCompleted) break;
        }
        await _semaphore.WaitAsync();
        bool hasItem;
        T item = default;
        lock (_queue)
        {
            hasItem = _queue.Count > 0;
            if (hasItem) item = _queue.Dequeue();
        }
        if (hasItem) yield return item;
    }
}

동일한 방법 으로 await및 의 공존에 유의하십시오 yield.

사용 예 (C # 8) :

var consumer = Task.Run(async () =>
{
    await foreach (var item in abc.GetConsumingEnumerable())
    {
        await Task.Delay(200);
        await Console.Out.WriteAsync(item + " ");
    }
});

메모 await전과를 foreach.


1
나중에 생각해 보면 클래스 이름 AsyncBlockingCollection이 무의미 하다고 생각합니다 . 이 두 개념은 정반대이기 때문에 무언가 비동기와 차단을 동시에 할 수는 없습니다!
Theodor Zoulias

0

약간의 해킹이 괜찮다면이 확장 프로그램을 사용해 볼 수 있습니다.

public static async Task AddAsync<TEntity>(
    this BlockingCollection<TEntity> Bc, TEntity item, CancellationToken abortCt)
{
    while (true)
    {
        try
        {
            if (Bc.TryAdd(item, 0, abortCt))
                return;
            else
                await Task.Delay(100, abortCt);
        }
        catch (Exception)
        {
            throw;
        }
    }
}

public static async Task<TEntity> TakeAsync<TEntity>(
    this BlockingCollection<TEntity> Bc, CancellationToken abortCt)
{
    while (true)
    {
        try
        {
            TEntity item;

            if (Bc.TryTake(out item, 0, abortCt))
                return item;
            else
                await Task.Delay(100, abortCt);
        }
        catch (Exception)
        {
            throw;
        }
    }
}

그래서 당신은 그것을 비동기로 만들기 위해 인공적인 지연을 가져 오나요? 아직도 막고있어?
nawfal
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.