【问题标题】:Is there anything like asynchronous BlockingCollection<T>?有没有类似异步 BlockingCollection<T> 的东西?
【发布时间】:2014-02-09 02:42:29
【问题描述】:

我想在BlockingCollection&lt;T&gt;.Take()的结果上异步await,所以我不阻塞线程。寻找这样的东西:

var item = await blockingCollection.TakeAsync();

我知道我可以这样做:

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

但这有点扼杀了整个想法,因为另一个线程(ThreadPool)反而被阻塞了。

还有其他选择吗?

【问题讨论】:

  • 我不明白,如果您使用await Task.Run(() =&gt; blockingCollection.Take()),任务将在其他线程上执行,您的UI线程不会阻塞。这不是重点吗?
  • @Selman22,这不是 UI 应用。它是一个导出基于Task 的API 的库。例如,它可以从 ASP.NET 中使用。有问题的代码在那里无法很好地扩展。
  • 如果ConfigureAwaitRun()之后使用还会有问题吗? [编辑。没关系,我明白你现在在说什么]

标签: c# .net collections task-parallel-library async-await


【解决方案1】:

我知道有四种选择。

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

下一个是来自TPL DataflowBufferBlock&lt;T&gt;。如果您只有一个消费者,您可以使用OutputAvailableAsyncReceiveAsync,或者将其链接到ActionBlock&lt;T&gt;。欲了解更多信息,see my blog

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

AsyncCollection&lt;T&gt;async 几乎等同于 BlockingCollection&lt;T&gt;,能够包装并发生产者/消费者集合,例如 ConcurrentQueue&lt;T&gt;ConcurrentBag&lt;T&gt;。您可以使用TakeAsync 异步使用集合中的项目。欲了解更多信息,see my blog

AsyncProducerConsumerQueue&lt;T&gt; 是一个更便携的async-兼容生产者/消费者队列。您可以使用DequeueAsync 异步使用队列中的项目。欲了解更多信息,see my blog

这些备选方案中的最后三个允许同步和异步 put 和 take。

【讨论】:

  • CodePlex 最终关闭时的 Git Hub 链接:github.com/StephenCleary/AsyncEx
  • API 文档中包含AsyncCollection.TryTakeAsync 方法,但是我在下载的Nito.AsyncEx.Coordination.dll 5.0.0.0(最新版本)中找不到。 package 中不存在引用的 Nito.AsyncEx.Concurrent.dll。我错过了什么?
  • @TheodorZoulias:该方法已在 v5 中删除。 v5 API 文档是here
  • 哦,谢谢。看起来这是枚举集合的最简单和最安全的方法。 while ((result = await collection.TryTakeAsync()).Success) { }。为什么它被删除了?
  • @TheodorZoulias:因为“尝试”对不同的人意味着不同的事情。我正在考虑重新添加一个“Try”方法,但它实际上具有与原始方法不同的语义。还要考虑在未来版本中支持异步流,如果支持,这绝对是最好的消费方法。
【解决方案2】:

...或者你可以这样做:

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 中添加的,这并不是那么简单。

【讨论】:

  • 无限for有什么用?如果信号量被释放,队列至少有一项要出队,不是吗?
  • @Blendester 如果多个消费者被阻止,可能会出现竞争情况。我们不能确定是否至少有两个竞争消费者,我们不知道他们是否都设法在他们对一个项目进行 deque 之前醒来。在比赛的情况下,如果一个人没有成功地去队列,它将重新进入睡眠状态并等待另一个信号。
  • 如果两个或多个消费者通过 WaitAsync(),那么队列中的项目数量相等,因此它们总是会成功出列。我错过了什么吗?
  • 这是一个阻塞集合,TryDequeue的语义是,带值返回,或者根本不返回。从技术上讲,如果您有超过 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. from docs.microsoft.com/en-us/dotnet/api/… 如何成功的WaitAsync 没有队列中的项目?如果 N 个发布唤醒的消费者超过 N 个,则semaphore 被破坏。不是吗?
【解决方案3】:

这是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 的发布,asynchronous enumeration 已成为内置语言功能。所需的类(IAsyncEnumerableIAsyncEnumerator)嵌入在 .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;
    }
}

注意awaityield在同一个方法中并存。

使用示例(C# 8):

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

注意foreach 之前的await

【讨论】:

  • 事后想到,我现在认为类名AsyncBlockingCollection 是荒谬的。有些东西不能同时是异步和阻塞的,因为这两个概念是完全相反的!
  • 但是,它仍然是 BlockingCollection 的异步版本 :)
【解决方案4】:

如果您不介意一些小技巧,可以尝试这些扩展。

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

【讨论】:

  • 所以你带来了人为的延迟来使其异步?它仍然在阻止对吗?
猜你喜欢
  • 1970-01-01
  • 2020-03-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多