web-dev-qa-db-fra.com

Existe-t-il quelque chose comme BlockingCollection asynchrone <T>?

Je voudrais await sur le résultat de BlockingCollection<T>.Take() de manière asynchrone, donc je ne bloque pas le thread. Vous cherchez quelque chose comme ça:

var item = await blockingCollection.TakeAsync();

Je sais que je pourrais faire ça:

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

mais cela tue un peu toute l'idée, car un autre thread (de ThreadPool) est bloqué à la place.

Y a-t-il une alternative?

68
avo

Je connais quatre alternatives.

Le premier est Channels , qui fournit une file d'attente threadsafe qui prend en charge les opérations asynchrones Read et Write. Les canaux sont hautement optimisés et prennent éventuellement en charge la suppression de certains éléments si un seuil est atteint.

Le suivant est BufferBlock<T> de Flux de données TPL . Si vous n'avez qu'un seul consommateur, vous pouvez utiliser OutputAvailableAsync ou ReceiveAsync, ou simplement le lier à un ActionBlock<T>. Pour plus d'informations, voir mon blog .

Les deux derniers sont des types que j'ai créés, disponibles dans ma bibliothèque AsyncEx .

AsyncCollection<T> est le async quasi-équivalent de BlockingCollection<T>, capable d'envelopper une collection simultanée de producteur/consommateur telle que ConcurrentQueue<T> ou ConcurrentBag<T>. Vous pouvez utiliser TakeAsync pour consommer de manière asynchrone des éléments de la collection. Pour plus d'informations, voir mon blog .

AsyncProducerConsumerQueue<T> est une file d'attente de producteurs/consommateurs compatible async plus portable. Vous pouvez utiliser DequeueAsync pour consommer de manière asynchrone des éléments de la file d'attente. Pour plus d'informations, voir mon blog .

Les trois dernières de ces alternatives permettent des mises et prises synchrones et asynchrones.

71
Stephen Cleary

... ou vous pouvez le faire:

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

Simple, entièrement fonctionnel asynchrone FIFO file d'attente.

Remarque: SemaphoreSlim.WaitAsync a été ajouté dans .NET 4.5 avant cela, ce n'était pas si simple.

15
John Leidegren

Voici une implémentation très basique d'un BlockingCollection qui supporte l'attente, avec beaucoup de fonctionnalités manquantes. Il utilise la célèbre classe AsyncEnumerable qui va devenir obsolète après la sortie de C # 8 (introduction des flux asynchrones), mais uniquement pour .NET Core 3.0. Le .NET Framework n'obtiendra pas cette mise à niveau, donc le AsyncEnumerable restera partiellement utile.

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

Exemple d'utilisation:

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

Production:

1 2 3 4 5 6 7 8 9 10

1
Theodor Zoulias

Si cela ne vous dérange pas un peu de piratage, vous pouvez essayer ces extensions.

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