.NET 中的 BlockingCollection 简介
BlockingCollection
是 System.Collections.Concurrent
命名空间下的一个类,顾名思义,与此命名空间下的任何其他集合一样,它也可以用于并发和多任务场景。 根据我的经验,很多开发者都熟悉 ConcurrentBag
、CuncurrentDictionary
、ConcurrentQueue
和 ConcurrentStack
。但是很少有人知道 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>
接口的并发集合的包装器。最著名的集合是 ConcurrentBag
、ConcurrentQueue
和 ConcurrentStack
。
以下代码是解决相同问题的类似解决方案,但这次使用 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
异常(如果集合为空)。 我们只需要处理这个异常并退出循环。
现在你可能会问,从集合中拾取的项的顺序是什么?
在本文开头,我提到 BlockingCollection
是 IProducerConsumerCollection<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.