为啥覆盖 Parallel.foreach 循环的 .NET 单元测试依赖于硬件?

Posted

技术标签:

【中文标题】为啥覆盖 Parallel.foreach 循环的 .NET 单元测试依赖于硬件?【英文标题】:Why is a .NET unit test covering a Parallel.foreach loop hardware-dependent?为什么覆盖 Parallel.foreach 循环的 .NET 单元测试依赖于硬件? 【发布时间】:2013-06-01 00:31:54 【问题描述】:

我正在使用 Moq 对一些包含 Parallel.foreach 循环的代码进行单元测试。

Arrange 阶段设置了 4 个要在循环中抛出的异常,然后包装在 AggregateException 中。

这在我的 i7 处理器上传递,我签入了代码。

之后,一位同事抱怨他没有通过。原来Parallel.foreach 在被炸毁之前只在他的 Core2duo 上产生了 2 个线程,因此在 AggregateException 中只包含了 2 个异常。

问题是如何解决这个问题,以便单元测试不依赖于处理器架构?几个想法:-

    有一个Microsoft article 手动添加异常到 AggregateException 但我们并不热衷于将其作为循环 如果出现问题,应尽早退出。 ParallelOptions.MaxDegreeOfParallelism可以 对使用的线程数设置上限。但除非这被拒绝 到 1(这似乎更像是作弊而不是正确的单元测试)如何 单元测试能否知道实际使用了多少线程? 因此正确设置 ArrangeAssert 阶段?

【问题讨论】:

子异常的数量不是必需的,不应以这种方式进行测试。这是一个实现细节。您可以检查 1+ 类型的正确异常。 @HenkHolterman 是的,但我必须适当地设置模拟。您是否建议我应该设置尽可能多的模拟调用,但不要断言所有这些实际上都在最后被调用? 我不确定你想模拟哪个部分,但没关系。 【参考方案1】:

您不应该测试类似的东西 - 它是实现细节。

事情是 - Parallel.ForEach 将处理元素,直到它得到异常。当发生异常时,它将停止处理任何新元素(但会完成对当前处理的元素的处理),然后抛出AgregateException

现在 - 您的 i7 CPU 有 4 个内核 + 超线程,这会导致产生更多线程进行处理,因此您可以获得更多异常(例如,发生异常时可以同时处理 4 件事)。但是在只有 2 个内核的 Core2Duo 上,只会同时处理 2 个项目(这是因为 TPL 足够聪明,只能创建足够多的线程来处理,而不是可用内核)。

实际发生 4 个异常的测试不会让您知道。它取决于机器。相反,您应该测试是否至少发生了一个异常 - 因为这是您所期望的。如果未来的用户将在旧的单核机器上运行您的代码,他将在AggregateException 中收到这一异常。

抛出异常的数量是特定于机器的,例如计算时间 - 您不会断言计算了多长时间,因此在这种情况下您不应断言异常数量。

【讨论】:

顺便说一句,这不仅仅是 CPU 的问题。具体的行为也可以根据同一进程中的 ThreadPool 上运行的其他内容、其他哪些进程正在使用 CPU、循环中的操作是否阻塞以及可能的其他事情而改变。 是的,完全正确。 CPU 关系只是最容易发现的一种,但您提到的所有内容也是有效的。 @Pako 谢谢 - 你的回答很好地解释了这个问题。据我了解,您的解决方案是设置模拟以允许最大数量的异常(=线程),但实际上并没有断言它们都发生了。这似乎有点不寻常,因为通常在模拟时我已经习惯了所有可验证的设置。 在进行并行处理时,您不能轻易假设某些事情会发生或不会发生,这让事情变得更加困难。相反,你专注于那些会告诉你某些事情正在发挥作用的迹象。这里的问题是Parallel.ForEach 一旦发现异常就停止处理,而您期望它处理所有项目。如果这不能满足您 - 您可能会退回到将最大并行度设置为 1 - 但这不是一个好的 IMO。您发现了异常 - 很酷,这就是您所期望的,因此您已经证明您的代码可以正常工作【参考方案2】:

单元测试可验证您期望发生的事情和实际发生的事情是否相同。这意味着您需要问自己您期望发生什么。

如果您期望处理中的所有潜在错误都会被报告,那么您的同事的运行实际上发现了您的代码中的错误。这意味着普通的 Parallel.ForEach() 对你不起作用,但是像 Parallel.ForEach() 这样带有 try-catch 和自定义异常处理的东西会。

如果你期望每个抛出异常都会被捕获,那么这就是你需要测试的。这可能会使您的测试复杂化,或者可能无法测试,具体取决于您如何抛出异常。对此的简单测试如下所示:

[Test]
public void ParallelForEachExceptionsTest()

    var exceptions = new ConcurrentQueue<Exception>();
    var thrown = Assert.Throws<AggregateException>(
        () => Parallel.ForEach(
            Enumerable.Range(1, 4), i =>
            
                var e = new Exception(i.ToString());
                exceptions.Enqueue(e);
                throw e;
            ));

    CollectionAssert.AreEquivalent(exceptions, thrown.InnerExceptions);

如果您期望的是当一个或多个异常被抛出时,那么至少其中一些会被捕获,那么这就是您应该测试的,并且您不必担心是否正确捕获了所有异常。

【讨论】:

非常感谢您在这里的努力。这是第二种情况 - 每个 throw 异常都必须被捕获。抛出的异常实际上来自对模拟对象的方法调用。所以正如你所说,我只需要验证其中的一个子集是否真的被抛出了。

以上是关于为啥覆盖 Parallel.foreach 循环的 .NET 单元测试依赖于硬件?的主要内容,如果未能解决你的问题,请参考以下文章

带有 BlockingCollection.GetConsumableEnumerable 的 Parallel.ForEach 循环

C#的并发循环(for,foreach,parallel.for,parallel.foreach)对比

.Net 中的多个 Parallel.ForEach 循环

Stringbuilder用于Parallel.Foreach循环

Parallel.ForEach 使用多线遍历循环

我在 Parallel.ForEach 循环中收到 TaskCanceledException,如何解决?