web-dev-qa-db-ja.com

非同期BlockingCollection <T>のようなものはありますか?

BlockingCollection<T>.Take()の結果を非同期でawaitしたいので、スレッドをブロックしません。このようなものを探しています:

var item = await blockingCollection.TakeAsync();

私はこれができることを知っています:

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

しかし、(ThreadPoolの)別のスレッドが代わりにブロックされるため、この種のアイデアは全体を殺します。

代替手段はありますか?

68
avo

私が知っている4つの選択肢があります。

1つは Channels です。これは、非同期のReadおよびWrite操作をサポートするスレッドセーフキューを提供します。チャネルは高度に最適化されており、オプションで、しきい値に達した場合に一部のアイテムをドロップできます。

次は、 TPL Dataflow からのBufferBlock<T>です。コンシューマが1つしかない場合は、OutputAvailableAsyncまたはReceiveAsyncを使用するか、単にActionBlock<T>にリンクできます。詳細については、 私のブログ を参照してください。

最後の2つは、作成したタイプで、 AsyncExライブラリ で使用できます。

AsyncCollection<T>BlockingCollection<T>とほぼ同等のasyncで、ConcurrentQueue<T>ConcurrentBag<T>などの並行プロデューサー/コンシューマーコレクションをラップできます。 TakeAsyncを使用して、コレクションのアイテムを非同期的に使用できます。詳細については、 私のブログ を参照してください。

AsyncProducerConsumerQueue<T> は、よりポータブルなasync互換のプロデューサ/コンシューマキューです。 DequeueAsyncを使用して、キューからアイテムを非同期的に消費できます。詳細については、 私のブログ を参照してください。

これらの選択肢の最後の3つは、同期および非同期のputおよびtakeを許可します。

71
Stephen Cleary

...またはこれを行うことができます:

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で追加されましたが、これはそれほど単純ではありませんでした。

15
John Leidegren

待機をサポートするBlockingCollectionの非常に基本的な実装を次に示します。多くの機能が欠落しています。有名な AsyncEnumerable クラスを使用しますが、これはC#8のリリース(非同期ストリームの導入)後に廃止される予定ですが、.NET Core 3.0専用です。 .NET Frameworkはこのアップグレードを取得しないため、AsyncEnumerableは部分的に有用なままです。

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);

出力:

1 2 3 4 5 6 7 8 9 10

1
Theodor Zoulias

ちょっとしたハックを気にしないのであれば、これらの拡張機能を試すことができます。

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;
        }
    }
}
1
Dejisys