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