是否有类似异步BlockingCollection <T>的东西?


85

我想await基于BlockingCollection<T>.Take()异步的结果,所以我不阻塞线程。寻找这样的事情:

var item = await blockingCollection.TakeAsync();

我知道我可以这样做:

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

但这有点使整个想法付诸东流,因为(的ThreadPool)另一个线程被阻塞了。

还有其他选择吗?


2
我不明白这一点,如果您使用await Task.Run(() => blockingCollection.Take())该任务将在其他线程上执行并且您的UI线程不会被阻塞,那不是重点吗?
SelmanGenç2014年

8
@ Selman22,这不是UI应用程序。这是一个Task基于库导出的API。例如,可以从ASP.NET使用它。有问题的代码在那里无法很好地扩展。
2014年

如果ConfigureAwaitRun()?之后使用仍然会是一个问题吗?[ed。没关系,我明白您现在在说什么]
MojoFilter '16

Answers:


94

我知道有四种选择。

第一个是Channels,它提供了一个支持异步ReadWrite操作的线程安全队列。通道经过高度优化,如果达到阈值,可以选择支持丢弃某些项目。

下一个BufferBlock<T>来自TPL Dataflow。如果您只有一个使用者,则可以使用OutputAvailableAsyncReceiveAsync,或仅将其链接到ActionBlock<T>。有关更多信息,请参阅我的博客

最后两个是我创建的类型,可在AsyncEx库中使用

AsyncCollection<T>async几乎相等BlockingCollection<T>,能够包装并发的生产者/消费者集合,例如ConcurrentQueue<T>ConcurrentBag<T>。您可以TakeAsync用来异步使用集合中的项目。有关更多信息,请参阅我的博客

AsyncProducerConsumerQueue<T>是更可移植的async兼容生产者/消费者队列。您可以DequeueAsync用来异步使用队列中的项目。有关更多信息,请参阅我的博客

这些选择的最后三个允许同步和异步放置。


12
当CodePlex最终关闭时的Git Hub链接:github.com/StephenCleary/AsyncEx
Paul

API文档包含该方法AsyncCollection.TryTakeAsync,但在下载的Nito.AsyncEx.Coordination.dll 5.0.0.0(最新版本)中找不到。程序包中不存在所引用的Nito.AsyncEx.Concurrent.dll。我想念什么?
Theodor Zoulias '19

@TheodorZoulias:该方法已在v5中删除。v5 API文档在此处
斯蒂芬·克莱里

哦谢谢。看来这是枚举集合的最简单,最安全的方法。while ((result = await collection.TryTakeAsync()).Success) { }。为什么将其删除?
Theodor Zoulias '19

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,带有值返回,或者根本不返回。从技术上讲,如果您有多个读取器,则同一读取器可以消耗两个(或多个)项目,然后其他任何读取器完全唤醒。成功WaitAsync只是一个信号,表明队列中可能有要消耗的物品,这不是保证。
约翰·莱德格伦

@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被破坏。是不是
Ashish Negi

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 3.0核心,和可供作为封装用于.NET框架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是荒谬的。某些东西不能同时异步和阻塞,因为这两个概念是完全相反的!
西奥多·祖利亚斯

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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.