在C#中,使用foreach是消耗BlockingCollection<T>的唯一方法吗?

4
我现在开始学习TPL。我在这个视频中看到了一个使用TPL的生产者/消费者模型简单版。

问题是:

以下代码:

BlockingCollection<Double> bc = new BlockingCollection<Double>(100);
IEnumerable<Double> d = bc.GetConsumingEnumerable();

返回一个可以使用foreach进行迭代(并自动消耗)的 IEnumerable<Double>

foreach (var item in d)
{
    // do anything with item
    // in the end of this foreach,
    // will there be any items left in d or bc? Why?
}

我的问题是:

  1. 如果我从 d 获取 IEnumerator<Double> dEnum = d.GetEnumerator()(例如用 while 循环遍历 d),那么 d.MoveNext() 也会消耗列表吗?(我的答案是:不会,因为 dEnum 并没有与 d 相关联,如果你知道我的意思的话。所以它将消耗 dEnum,但不是 d,甚至也不是 bc
  2. 我可以以其他方式循环遍历 bc(或 d)而不是使用 foreach 循环并消耗项吗?(使用 while 循环比 foreach 循环快得多,我担心在科学计算问题中出现性能问题)
  3. BlockingCollection<T> 类型中的“消耗”是什么意思?

例如,代码:

IEnumerator<Double> dEnum = d.GetEnumerator();
while (dEnum.MoveNext())
{
    // do the same with dEnum.Current as
    // I would with item in the foreach above...
}

感谢大家提前阅读!

你为什么认为 foreach 会有性能问题? - Reed Copsey
好的,我做了一些非常简单的测试(比如迭代一个有数百万个元素的数组,并对数组中每个项执行完全相同的条件检查),并测量了while循环、for循环和foreach循环完成任务所需的时间... 最终,while循环明显更快... - Girardi
2个回答

4
< p >使用foreach没有任何“性能问题”。直接使用枚举器与直接使用foreach循环相比,不太可能在性能上有任何可衡量的改进。

话虽如此,GetConsumingEnumerable()返回一个标准的IEnumerable<T>,因此您可以按任何方式枚举它。获取IEnumerator<T>并直接枚举它仍然可以正常工作。

请注意,如果您不想使用GetConsumingEnumerable(),则可以直接使用ConcurrentQueue<T>。默认情况下,BlockingCollection<T>包装了一个ConcurrentQueue<T>,而实际上只是提供了一个更简单的API(GetConsumingEnumerable()),以使生产者/消费者方案更容易编写。直接使用ConcurrentQueue<T>会更接近于使用BlockingCollection<T>而不使用可枚举对象。


正如我回复@Reed的评论所说,我进行了一些虚拟测试,while似乎更快...至少在处理非并行问题时是这样的... - Girardi
@Girardi 通常,编程语言本身的性能差异与进行的任何计算相比会相形见绌。 如果你正在使用像 BlockingCollection<T> 这样的东西 - 此时 while / for / foreach 的差异与将项目放入和取出集合的开销相比微不足道。 因此,在优化时不应重点关注这一点 - 应专注于找到并修复“真正”的性能问题(在使用项目的代码中)。 - Reed Copsey
BlockingCollection 不仅仅是 ConcurrentQueue(或其他并发集合)的简单 API。它的重点在于其方法会阻塞,直到可以执行操作。例如,如果您想要使用仅 ConcurrentQueue 实现 BlockingCollection.Take() 的替代方案,则需要手动使用一些同步构造(如 ManualResetEvent)。 - svick
@svick 是的-抱歉,我意识到我的措辞在那里并不完全清晰。BC将“阻止”部分添加到ConcurrentQueue中,包括.Take和.GetConsumingEnumerable。如果您想要阻塞性质,那么是的,您需要它。但是,如果您想要这个,GetConsumingEnumerable可能是您想要的选项。 - Reed Copsey

4
如果我从d获取IEnumerator<Double> dEnum = d.GetEnumerator()(例如用while循环迭代d),d.MoveNext()会消耗列表吗?
绝对会。这正是foreach循环所做的。
我可以以不同于foreach循环的方式循环遍历bc(或d)并消耗项目吗?(while循环比foreach循环快得多,我担心在科学计算问题上存在性能问题)
如果您的while循环更快,那说明您做错了什么。它们应该完全相同 - 除了foreach循环还将处理迭代器,您也应该这样做...
如果您可以发布一个简短但完整的程序来演示此差异,我们可以更详细地查看它。
另一种选择是使用Take(和类似的方法)。
BlockingCollection类型中的消耗到底意味着什么?
实际上,“从集合中删除下一个项”。

正如我回复@Reed的评论时所说,我进行了一些虚拟测试,while似乎更快...至少在处理非并行问题时是这样的... - Girardi
2
@Girardi:你的评论谈到了对数组进行迭代。你有实际迭代过BlockingCollection<T>吗?此外,很容易在基准测试中犯错。这就是为什么我要求提供一个简短但完整的程序来演示问题——基于错误的假设走上完全不同的路线是没有意义的。 - Jon Skeet
1
@Girardi:一如既往地,先让代码以简单的方式运行起来,然后再衡量性能。 - Jon Skeet
1
@Girardi:它将其从阻塞集合中移除。这就是为什么它是“可消耗的枚举”。从GetConsumingEnumerable文档中可以看到:“一个IEnumerable<T>,它会从集合中移除并返回项。” - Jon Skeet
我在BlockingCollection<T>中使用foreachwhile进行了基准测试,并且以并行方式运行。令我惊讶的是,foreachwhile快10倍! - Girardi
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接