Jon*_*Jon 70

你确实可以使用a BlockingCollection,但这样做绝对没有意义.

首先,请注意,它BlockingCollection是实现的集合的包装器IProducerConsumerCollection<T>.实现该接口的任何类型都可以用作底层存储:

创建BlockingCollection<T>对象时,不仅可以指定有界容量,还可以指定要使用的集合类型.例如,您可以ConcurrentQueue<T>为先进先出(FIFO)行为或ConcurrentStack<T>最后进先出(LIFO)行为的对象指定对象.您可以使用任何实现该IProducerConsumerCollection<T>接口的集合类.BlockingCollection<T>is 的默认集合类型ConcurrentQueue<T>.

这包括ConcurrentBag<T>,这意味着您可以拥有阻止并发包.那么普通IProducerConsumerCollection<T>和阻挡系列之间的区别是什么?BlockingCollection说的文件(强调我的):

BlockingCollection<T>用作IProducerConsumerCollection<T>实例的包装器 ,允许从集合中删除尝试以阻止数据可用于删除.类似地,BlockingCollection<T>可以创建执行的上界上允许的数据元素的数量IProducerConsumerCollection<T>[...]

因为在链接的问题中没有必要做这些事情中的任何一个,使用BlockingCollection简单地添加一层未使用的功能.

  • @ Jon,谢谢,这对我帮助很大,使我摆脱了白痴状态,并且在我真正需要 ConcurrentDictionary 时不再浪费时间学习 ConcurrentBag 和 BlockingCollection (3认同)

The*_*ias 20

每当您发现需要线程安全时List<T>,在大多数情况下, 和 都不ConcurrentBag<T>BlockingCollection<T>您的最佳选择。这两个集合都专门用于促进生产者-消费者场景,因此除非您有多个线程同时在集合中添加删除项目,否则您应该寻找其他选项(在ConcurrentQueue<T>大多数情况下最好的候选者)。

特别是对于ConcurrentBag<T>,它是一个针对混合生产者-消费者场景的极其专业的类。这意味着每个工作线程都应该既是生产者又是消费者(从同一集合中添加和删除项目)。可能是类内部存储的一个很好的候选者ObjectPool,但除此之外,很难想象该类有任何有利的使用场景。

人们通常认为 theConcurrentBag<T>是 a 的线程安全等价物List<T>,但事实并非如此。这两个 API 的相似性具有误导性。调用AddaList<T>会导致在列表末尾添加一个项目。调用Add结果ConcurrentBag<T>而不是添加到包内随机插槽中的项目。本质ConcurrentBag<T>上是无序的。它没有针对枚举进行优化,并且在被命令这样做时做得很糟糕。它在内部维护了一堆线程本地队列,因此其内容的顺序由哪个线程做了什么决定,而不是由何时发生某事决定。在每次枚举之前ConcurrentBag<T>,所有这些线程本地队列都被复制到一个数组中,这给垃圾收集器增加了压力(源代码)。例如,该行var item = bag.First();会生成整个集合的副本,仅返回一个元素。

这些特性使得/循环ConcurrentBag<T>结果的存储不太理想。Parallel.ForParallel.ForEach

更好的线程安全替代品List<T>.AddConcurrentQueue<T>.Enqueue方法。“Enqueue”这个词比“Add”不太熟悉,但它实际上做了您期望它做的事情。

没有什么是他ConcurrentBag<T>能做而他ConcurrentQueue<T>不能做的。例如,这两个集合都没有提供从集合中删除特定项目的方法。如果您想要一个TryRemove带有key参数的方法的并发集合,您可以查看该类ConcurrentDictionary<K,V>

ConcurrentBag<T>经常出现在 Microsoft 文档中与任务并行库相关的示例中。比如这里。无论谁编写了文档,显然他们更看重的是编写Add而不是使用的微小可用性优势Enqueue,而不是使用错误集合的行为/性能劣势。考虑到这些示例是在 TPL 刚刚推出时编写的,并且目标是让大多数不熟悉并行编程的开发人员快速采用该库,这是有道理的。我明白了,Enqueue当你第一次看到它时,这是一个可怕的词。不幸的是,现在有一整代开发人员已经将这些纳入了ConcurrentBag<T>他们的心理工具中,尽管考虑到这个集合的专业性,它没有必要存在在那里。

Parallel.ForEach如果您想以与源元素完全相同的顺序收集循环结果,则可以使用List<T>带有lock. 在大多数情况下,开销可以忽略不计,特别是当循环内的工作量很大时。一个例子如下所示:

List<TResult> results = new();

Parallel.ForEach(source, parallelOptions, (item, state, index) =>
{
    TResult result = GetResult(item);
    lock (results)
    {
        while (results.Count <= index) results.Add(default);
        results[(int)index] = result;
    }
});
Run Code Online (Sandbox Code Playgroud)

source这是针对大小未知的延迟序列的情况。如果你事先知道它的大小,那就更简单了。只需预分配一个TResult[]数组,然后并行更新它而无需锁定:

TResult[] results = new TResult[source.Count];

Parallel.ForEach(source, parallelOptions, (item, state, index) =>
{
    results[index] = GetResult(item);
});
Run Code Online (Sandbox Code Playgroud)

TPL 在任务执行结束时包含内存屏障,因此results数组的所有值对于当前线程都可见(引用)。


Ahm*_*lan 16

  • List<T> 是一个旨在用于单线程应用程序的集合.

  • ConcurrentBag<T>是一种子类型,Collections.Concurrent旨在简化在多线程环境中使用集合.如果使用ConcurrentCollection,则不必锁定集合以防止其他线程损坏.您可以从集合中插入或获取数据,而无需编写特殊的锁定代码.

  • BlockingCollection<T>旨在消除检查线程之间的共享集合中是否有新数据的要求.如果有新数据插入到共享集合中,那么您的消费者线程将会无法清醒地唤醒.因此,您不必检查通常在while循环中的某些时间间隔内是否有新数据可供消费者线程使用.