Чи є щось на зразок асинхронного BlockingCollection <T>?


86

Я б хотів await про результат BlockingCollection<T>.Take()асинхронно, тому я не блокую потік. Шукаємо щось подібне:

var item = await blockingCollection.TakeAsync();

Я знаю, що міг це зробити:

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

але це як би вбиває всю ідею, оскільки ThreadPoolзамість цього блокується інший потік (of ).

Чи є якась альтернатива?


2
Я не розумію цього, якщо ви використовуєте await Task.Run(() => blockingCollection.Take())завдання, воно буде виконано в іншому потоці, і ваш потік інтерфейсу не буде заблокований.
Сельман Генч,

8
@ Selman22, це не програма інтерфейсу користувача. Це бібліотека, що експортує TaskAPI. Наприклад, його можна використовувати з ASP.NET. Код, про який йде мова, там би погано масштабувався.
avo

Чи все одно це буде проблемою, якби ConfigureAwaitйого використали після Run()? [ред. неважливо, я бачу, що ви зараз говорите]
MojoFilter

Відповіді:


96

Я знаю чотири альтернативи.

Перший - це Channels , який забезпечує безпечну чергу потоків, яка підтримує асинхронні Readта Writeопераційні дії. Канали високо оптимізовані і, за бажання, підтримують скидання деяких елементів, якщо досягнуто порогове значення.

Наступне - BufferBlock<T>з потоку даних TPL . Якщо у вас є тільки один споживач, ви можете використовувати OutputAvailableAsyncабо ReceiveAsync, або просто зв'язати його з ActionBlock<T>. Для отримання додаткової інформації див. Мій блог .

Останні два типи, які я створив, доступні в моїй бібліотеці AsyncEx .

AsyncCollection<T>є asyncмайже еквівалентом BlockingCollection<T>, здатним обгортати одночасну колекцію виробників / споживачів, таких як ConcurrentQueue<T>або ConcurrentBag<T>. Ви можете використовувати TakeAsyncдля асинхронного споживання предметів з колекції. Для отримання додаткової інформації див. Мій блог .

AsyncProducerConsumerQueue<T>- це більш портативна asyncсумісна черга виробників / споживачів. Ви можете використовувати DequeueAsyncдля асинхронного споживання елементів із черги. Для отримання додаткової інформації див. Мій блог .

Останні три з цих альтернатив дозволяють синхронні та асинхронні пути і дуби.


12
Посилання на Git Hub, коли CodePlex остаточно вимкнеться: github.com/StephenCleary/AsyncEx
Пол

Документація API містить метод AsyncCollection.TryTakeAsync, але я не можу знайти його в завантаженій Nito.AsyncEx.Coordination.dll 5.0.0.0(остання версія). Посилання Nito.AsyncEx.Concurrent.dll у пакеті не існує . Чого мені не вистачає?
Теодор Зуліас,

@TheodorZoulias: Цей метод було вилучено у версії 5. Документи API v5 тут .
Стівен Клірі

О, дякую. Схоже, це був найпростіший і найбезпечніший спосіб перерахувати колекцію. while ((result = await collection.TryTakeAsync()).Success) { }. Чому його видалили?
Теодор Zoulias

1
@TheodorZoulias: Тому що "Спробувати" означає різні речі для різних людей. Я думаю про те, щоб додати метод "Спробувати", але він насправді мав би іншу семантику, ніж початковий метод. Також розглядається підтримка асинхронних потоків у майбутній версії, що, безумовно, було б найкращим способом споживання, коли підтримується.
Стівен Клірі

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? якщо семафор випущений, у черзі є щонайменше один елемент для зняття, ні?
Blendester

2
@Blendester, якщо заблоковано кілька споживачів, може існувати перегоновий стан. Ми не можемо точно знати, що немає принаймні двох конкуруючих споживачів, і ми не знаємо, чи вдається їм обом прокинутися, перш ніж дістати товар. У випадку перегонів, якщо хтось не зумів вивести, він повернеться спати і чекати іншого сигналу.
Джон Лейдегрен

Якщо два або більше споживачів проходять через WaitAsync (), тоді в черзі є еквівалентна кількість елементів, і, отже, вони завжди успішно знімуть чергу. Мені чогось не вистачає?
mindcruzer

2
Це блокуюча колекція, семантика TryDequeueare, повертається зі значенням або взагалі не повертається. Технічно, якщо у вас більше 1 зчитувача, той самий зчитувач може споживати два (або більше) предмети до того, як будь-який інший зчитувач повністю прокинеться. Успіх WaitAsync- це лише сигнал про те, що в черзі можуть бути предмети для споживання, це не гарантія.
Джон Лейдегрен

@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.з docs.microsoft.com/en-us/dotnet/api/... Як успішний WaitAsyncне має елементів у черзі? Якщо випуск N прокидається більше ніж N споживачів, ніж semaphoreпорушується. Чи не так?
Ашіш Негі

4

Ось дуже базова реалізація a, BlockingCollectionяка підтримує очікування, з великою кількістю відсутніх функцій. Він використовує AsyncEnumerableбібліотеку, яка робить можливим асинхронне перерахування для версій C #, старших за 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);
            }
        });
    }
}

Приклад використання:

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 асинхронне перерахування стало вбудованою мовною функцією. Необхідні класи ( IAsyncEnumerable, IAsyncEnumerator) вбудовані в .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;
    }
}

Зверніть увагу на співіснування одного awaitі yieldтого ж методу.

Приклад використання (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є безглуздою. Щось не може бути асинхронним і блокуючим одночасно, оскільки ці два поняття є точними протилежностями!
Теодор Зуліас,

0

Якщо ви не проти трохи зламати, ви можете спробувати ці розширення.

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

Отже, ви приносите штучну затримку, щоб зробити її асинхронною? Це все ще блокує вірно?
nawfal
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.