BlockingCollectionSystem.Collections.Concurrent 命名空间下的一个类,顾名思义,与此命名空间下的任何其他集合一样,它也可以用于并发和多任务场景。 根据我的经验,很多开发者都熟悉 ConcurrentBagCuncurrentDictionaryConcurrentQueueConcurrentStack。但是很少有人知道 BlockingCollection 的威力和用途。

在继续之前,让我们看一下使用并发队列的经典示例。想象一下这个场景,我们有几个线程将用户的电子邮件添加到队列中,并且有一个线程从队列中读取电子邮件并向他们发送电子邮件。

点击查看代码
class EmailService
{
    private ConcurrentQueue<string> _queue = new ConcurrentQueue<string>();
 
    public void AddEmail(string email)
    {
        _queue.Enqueue(email);
    }
 
    public void StartSendingEmail()
    {
        while (true)
        {
            bool isNotEmpty = _queue.TryDequeue(out string email);
 
            if (isNotEmpty)
            {
                SendEmail(email);
            }
            else
            {
                Thread.Sleep(1000);
            }
        }
    }
 
    private void SendEmail(string email)
    {
        //Send email here
    }
}

在前面的代码中,我们有一个 AddEmail 方法,不同的线程使用该方法将项目添加到队列中。在 StartSendingEmail 方法中,我们首先尝试从队列中挑选一封电子邮件并发送一封电子邮件。如果队列中没有电子邮件,我们将等待 1000 毫秒,然后再次尝试从列表中选择一个新项目(如果存在)。这种情况一直在继续。

我们在这里使用一种简单的轮询技术。这里的问题是如何计算出这 1000 毫秒。我们不知道何时可以将电子邮件添加到队列中以进行取件。 .NET Framework 中还有一些其他旧的线程类可以一起使用并解决这个问题,但在这里我们将利用 BlockingCollection

BlockingCollection 实际上是实现了 IProducerConsumerCollection<T> 接口的并发集合的包装器。最著名的集合是 ConcurrentBagConcurrentQueueConcurrentStack

以下代码是解决相同问题的类似解决方案,但这次使用 BlocingCollection

点击查看代码
class EmailService
{
    private BlockingCollection<string> _collection = new BlockingCollection<string>();
 
    public void AddEmail(string email)
    {
        _collection.Add(email);
    }
    public void StartSendingEmail()
    {
        while (true)
        {
            string email = _collection.Take();
 
            SendEmail(email);
        }
    }
 
    private void SendEmail(string email)
    {
        //Send email here
    }
}

查看 StartSendingEmail 方法,看看它是如何被简化的。

BlocingCollection 提供了一个名为 Take 的方法。 此方法从集合中返回(移除)一个项目(如果存在),否则会阻塞线程,直到将来有新项目可用(这意味着稍后会将新电子邮件添加到集合中)。 所以我们不再需要暂停操作1秒再开始轮询,甚至不用关心集合是否为空。

还有一种方法可以通知 BlocingCollection 不会将新电子邮件添加到集合中。 这意味着如果 BlocingCollection 已经为空,则无需再等待新项目。 这可以通过调用 CompleteAdding 方法来完成。 调用此方法后,如果调用 Take 方法集合为空,则会抛出 InvalidOperationException

让我们在 FinishSendingEmail 的代码中添加另一个功能。

点击查看代码
public void FinishSendingEmail()
{
    _collection.CompleteAdding();
}
 
public void StartSendingEmail()
{
    while (true)
    {
        try
        {
            string email = _collection.Take();
 
            SendEmail(email);
        }
        catch (InvalidOperationException)
        {
            // we are done!
            return;
        }
    }
}

通过调用 FinishSendingEmail 方法,Take 方法会抛出 InvalidOperationException 异常(如果集合为空)。 我们只需要处理这个异常并退出循环。

现在你可能会问,从集合中拾取的项的顺序是什么?

在本文开头,我提到 BlockingCollectionIProducerConsumerCollection<T> 实现的包装器。 BlockingCollection 默认使用 ConcurrentQueue 作为底层数据源。 但是我们可以明确指定应该使用哪个数据源。 这意味着如果我们希望以 LIFO(后进先出)顺序获取项目(此处为电子邮件),只需在初始化时将 ConcurrentStack 的实例发送到集合即可:
BlockingCollection<string> _collection = new BlockingCollection<string>(new ConcurrentStack<string>());

在这里,我们讨论了 BlockingCollection 的一些基本特性,但是这个类仍然提供了很多。 例如,可以将 CancellationToken 作为参数发送到 Take 方法。


原文:https://weblogs.asp.net/morteza/an-introduction-to-blockingcollection

补充:

  • BlockingCollection is a thread-safe collection class that provides the following features:

  • An implementation of the Producer-Consumer pattern.

  • Concurrent adding and taking of items from multiple threads.

  • Optional maximum capacity.

  • Insertion and removal operations that block when collection is empty or full.

  • Insertion and removal "try" operations that do not block or that block up to a specified period of time.

  • Encapsulates any collection type that implements IProducerConsumerCollection

  • Cancellation with cancellation tokens.

  • Two kinds of enumeration with foreach (For Each in Visual Basic):

    1. Read-only enumeration.

    2. Enumeration that removes items as they are enumerated.