C'è qualcosa come BlockingCollection <T> asincrono?


87

Vorrei awaitsul risultato in BlockingCollection<T>.Take()modo asincrono, quindi non blocco il thread. Alla ricerca di qualcosa di simile:

var item = await blockingCollection.TakeAsync();

So che potrei farlo:

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

ma questo uccide l'intera idea, perché un altro thread (di ThreadPool) viene invece bloccato.

C'è qualche alternativa?


3
Non capisco, se usi await Task.Run(() => blockingCollection.Take())l'attività verrà eseguita su un altro thread e il tuo thread dell'interfaccia utente non verrà bloccato. Non è questo il punto?
Selman Genç

8
@ Selman22, questa non è un'app dell'interfaccia utente. È Taskun'API basata sull'esportazione di una libreria . Può essere utilizzato da ASP.NET, ad esempio. Il codice in questione non sarebbe scalabile bene lì.
evitare il

Sarebbe ancora un problema se ConfigureAwaitfosse usato dopo il Run()? [ed. non importa, vedo cosa stai dicendo ora]
MojoFilter

Risposte:


99

Ci sono quattro alternative che conosco.

Il primo è Canali , che fornisce una coda sicura per i thread che supporta operazioni asincrone Reade Write. I canali sono altamente ottimizzati e facoltativamente supportano l'eliminazione di alcuni elementi se viene raggiunta una soglia.

Il prossimo è BufferBlock<T>da TPL Dataflow . Se hai un solo consumatore, puoi usare OutputAvailableAsynco ReceiveAsynco semplicemente collegarlo a un file ActionBlock<T>. Per ulteriori informazioni, vedere il mio blog .

Gli ultimi due sono tipi che ho creato, disponibili nella mia libreria AsyncEx .

AsyncCollection<T>è il asyncquasi equivalente di BlockingCollection<T>, in grado di avvolgere una raccolta simultanea produttore / consumatore come ConcurrentQueue<T>o ConcurrentBag<T>. È possibile utilizzare TakeAsyncper consumare in modo asincrono gli elementi della raccolta. Per ulteriori informazioni, vedere il mio blog .

AsyncProducerConsumerQueue<T>è una asynccoda produttore / consumatore più portabile e compatibile. È possibile utilizzare DequeueAsyncper consumare in modo asincrono elementi dalla coda. Per ulteriori informazioni, vedere il mio blog .

Le ultime tre di queste alternative consentono put e take sincroni e asincroni.


12
Collegamento a Git Hub per quando CodePlex si arresta definitivamente: github.com/StephenCleary/AsyncEx
Paul

La documentazione API contiene il metodo AsyncCollection.TryTakeAsync, ma non riesco a trovarlo nella versione scaricata Nito.AsyncEx.Coordination.dll 5.0.0.0(ultima versione). Il Nito.AsyncEx.Concurrent.dll a cui si fa riferimento non esiste nel pacchetto . Cosa mi manca?
Theodor Zoulias

@ TheodorZoulias: quel metodo è stato rimosso nella v5. I documenti dell'API v5 sono qui .
Stephen Cleary

Oh, grazie. Sembra che sia stato il modo più semplice e sicuro per enumerare la raccolta. while ((result = await collection.TryTakeAsync()).Success) { }. Perché è stato rimosso?
Theodor Zoulias

1
@ TheodorZoulias: Perché "Try" significa cose diverse per persone diverse. Sto pensando di aggiungere di nuovo un metodo "Try" ma in realtà avrebbe una semantica diversa rispetto al metodo originale. Guardando anche al supporto dei flussi asincroni in una versione futura, che sarebbe sicuramente il miglior metodo di consumo se supportato.
Stephen Cleary

21

... oppure puoi farlo:

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

Coda FIFO asincrona semplice e completamente funzionale.

Nota: è SemaphoreSlim.WaitAsyncstato aggiunto in .NET 4.5 in precedenza, non era così semplice.


2
A che serve infinito for? se il semaforo viene rilasciato, la coda ha almeno un elemento da rimuovere dalla coda, no?
Blendester

2
@Blendester potrebbe esserci una condizione di gara se più consumatori vengono bloccati. Non possiamo sapere con certezza che non ci siano almeno due consumatori concorrenti e non sappiamo se entrambi riescono a svegliarsi prima di poter togliere un articolo. In caso di gara, se non si riesce a staccare, si torna a dormire e si attende un altro segnale.
John Leidegren

Se due o più consumatori superano WaitAsync (), nella coda è presente un numero equivalente di elementi e pertanto verranno sempre rimossi dalla coda correttamente. Mi sto perdendo qualcosa?
mindcruzer

2
Questa è una raccolta di blocchi, la semantica di TryDequeueare, restituire con un valore o non restituire affatto. Tecnicamente, se hai più di 1 lettore, lo stesso lettore può consumare due (o più) elementi prima che qualsiasi altro lettore sia completamente sveglio. Un successo WaitAsyncè solo un segnale che potrebbero esserci articoli in coda da consumare, non è una garanzia.
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.da docs.microsoft.com/en-us/dotnet/api/… In che modo un successo WaitAsyncnon ha elementi in coda? Se il rilascio N si sveglia più di N consumatori, quello che semaphoreè rotto. Non è vero?
Ashish Negi

4

Ecco un'implementazione molto semplice di a BlockingCollectionche supporta l'attesa, con molte funzionalità mancanti. Usa la AsyncEnumerablelibreria, che rende possibile l'enumerazione asincrona per le versioni C # precedenti alla 8.0.

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

Esempio di utilizzo:

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

Produzione:

1 2 3 4 5 6 7 8 9 10


Aggiornamento: con il rilascio di C # 8, l'enumerazione asincrona è diventata una funzionalità del linguaggio incorporata. Le classi richieste ( IAsyncEnumerable, IAsyncEnumerator) sono incorporate in .NET Core 3.0 e sono offerte come pacchetto per .NET Framework 4.6.1+ ( Microsoft.Bcl.AsyncInterfaces ).

Ecco GetConsumingEnumerableun'implementazione alternativa , con la nuova sintassi 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;
    }
}

Notare la coesistenza di awaite yieldnello stesso metodo.

Esempio di utilizzo (C # 8):

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

Nota il awaitprima del foreach.


1
Come ripensamento, ora penso che il nome della classe non AsyncBlockingCollectionabbia senso. Qualcosa non può essere asincrono e bloccante allo stesso tempo, poiché questi due concetti sono gli opposti esatti!
Theodor Zoulias

-2

Se non ti dispiace un piccolo hack, puoi provare queste estensioni.

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
Quindi porti un ritardo artificiale per renderlo asincrono? Sta ancora bloccando, giusto?
nawfal
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.