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


87

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

var item = await blockingCollection.TakeAsync();

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

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

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

代替手段はありますか?


3
await Task.Run(() => blockingCollection.Take())タスクを使用すると他のスレッドで実行され、UIスレッドがブロックされないため、これはわかりません。それがポイントではありませんか?
セルマンGENC

8
@ Selman22、これはUIアプリではありません。これは、ライブラリエクスポートTaskベースのAPIです。たとえば、ASP.NETから使用できます。問題のコードはそこではうまくスケーリングしません。
avo 2014年

ConfigureAwait後に使用された場合でも問題はありRun()ますか?[編 気にしないでください、あなたが今言っていることがわかります]
MojoFilter 2016年

回答:


99

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

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

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

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

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

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

これらの選択肢の最後の3つは、同期および非同期のプットとテイクを可能にします。


12
CodePlexが最終的にシャットダウンしたときのGitHub
Paul

APIドキュメントにメソッドAsyncCollection.TryTakeAsyncが含まれていますが、ダウンロードしたNito.AsyncEx.Coordination.dll 5.0.0.0(最新バージョン)で見つかりません。参照されているNito.AsyncEx.Concurrent.dllパッケージに存在しません。何が足りないのですか?
TheodorZoulias19年

@TheodorZoulias:そのメソッドはv5で削除されました。v5APIドキュメントはこちらです。
スティーブンクリアリー

ああ、ありがとう。コレクションを列挙するのに最も簡単で安全な方法だったようです。while ((result = await collection.TryTakeAsync()).Success) { }。なぜ削除されたのですか?
TheodorZoulias19年

1
@TheodorZoulias:「試してみる」とは人によって意味が異なるからです。「Try」メソッドを再び追加することを考えていますが、実際には元のメソッドとは異なるセマンティクスを持っています。また、将来のバージョンで非同期ストリームをサポートすることも検討しています。これは、サポートされている場合、間違いなく最良の消費方法です。
スティーブンクリアリー

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ですか?セマフォが解放された場合、キューにはデキューするアイテムが少なくとも1つあります。
Blendester

2
@Blendester複数のコンシューマーがブロックされている場合、競合状態が発生する可能性があります。競合する消費者が少なくとも2人いないことは確かではなく、両方がアイテムをデキューする前に目を覚ますことができたかどうかもわかりません。レースが発生した場合、デキューできなかった場合は、スリープ状態に戻り、別の信号を待ちます。
JohnLeidegren19年

2つ以上のコンシューマーがWaitAsync()を通過した場合、キューには同数のアイテムがあるため、常に正常にデキューされます。私は何かが足りないのですか?
mindcruzer

2
これはブロッキングコレクションであり、TryDequeueareのセマンティクスは、値を返すか、まったく返さないかのいずれかです。技術的には、複数のリーダーがある場合、他のリーダーが完全に起動する前に、同じリーダーが2つ(またはそれ以上)のアイテムを消費する可能性があります。成功WaitAsyncは、キューに消費するアイテムがある可能性があることを示す単なるシグナルであり、保証ではありません。
JohnLeidegren19年

@JohnLeidegrenIf 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.からdocs.microsoft.com/en-us/dotnet/api/... 成功している方法はWaitAsync、キュー内のアイテムを持っていませんか?N個のリリースがN個以上のコンシューマーを起動した場合、中断さsemaphoreれます。そうですね。
AshishNegi20年

4

これは、BlockingCollection待機をサポートする非常に基本的な実装ですが、多くの機能が不足しています。これはAsyncEnumerableライブラリを使用します。これにより、8.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);

出力:

1 2 3 4 5 6 7 8 9 10


更新: C#8のリリースにより、非同期列挙組み込み言語機能になりました。必要なクラス(IAsyncEnumerableIAsyncEnumerator)は.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;
    }
}

同じ方法でawaityieldが共存していることに注意してください。

使用例(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は無意味だと思います。これらの2つの概念は正反対であるため、非同期とブロッキングを同時に行うことはできません。
TheodorZoulias20年

-2

少しハックしてもかまわない場合は、これらの拡張機能を試すことができます。

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
それで、あなたはそれを非同期にするために人為的な遅延をもたらしますか?それはまだブロックしていますよね?
nawfal
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.