4

为了加快 C# 中物理对象的处理速度,我决定将线性更新算法更改为并行算法。我认为最好的方法是使用 ThreadPool,因为它是为完成作业队列而构建的。

当我第一次实现并行算法时,我为每个物理对象排队了一个作业。请记住,单个作业完成得相当快(更新力、速度、位置,检查与任何周围对象的旧状态的碰撞以使其线程安全等)。然后,我将使用单个等待句柄等待所有作业完成,每次物理对象完成时我都会递减一个互锁整数(在达到零时,我然后设置等待句柄)。需要等待,因为我需要做的下一个任务涉及更新所有对象。

我注意到的第一件事是表演很疯狂。平均下来,线程池的速度似乎快了一点,但性能却出现了巨大的峰值(每次更新大约 10 毫秒,随机跳转到 40-60 毫秒)。我尝试使用 ANTS 对此进行分析,但是我无法深入了解为什么会出现尖峰。

我的下一个方法是仍然使用 ThreadPool,但是我将所有对象分成组。我最初只使用 8 个组,因为这就是我的计算机所具有的任何内核。表演很棒。它远远优于单线程方法,并且没有峰值(每次更新大约 6 毫秒)。

我唯一想到的是,如果一个工作在其他工作之前完成,就会有一个空闲的核心。因此,我将作业数量增加到 20 个左右,甚至增加到 500 个。正如我所料,它下降到 5ms。

所以我的问题如下:

  • 当我快速/大量地调整作业大小时,为什么会出现尖峰?
  • 是否有任何关于如何实现 ThreadPool 的见解可以帮助我了解如何最好地使用它?
4

5 回答 5

4

使用线程是有代价的——你需要上下文切换,你需要锁定(当一个线程试图获取一个新作业时,作业队列很可能被锁定)——这一切都是有代价的。与您的线程正在做的实际工作相比,这个价格通常很小,但如果工作很快结束,价格就会变得有意义。

您的解决方案似乎是正确的。一个合理的经验法则是线程数是内核数的两倍。

于 2012-09-08T19:25:57.607 回答
3

正如您可能期望的那样,峰值可能是由管理线程池并将任务分配给它们的代码引起的。

对于并行编程,有比“手动”跨不同线程分配工作更复杂的方法(即使使用线程池)。

例如,请参阅.NET Framework 中的并行编程以获取概述和不同选项。在您的情况下,“解决方案”可能就像这样简单:

Parallel.ForEach(physicObjects, physicObject => Process(physicObject));
于 2012-09-08T19:31:07.327 回答
2

以下是我对你的两个问题的看法:

我想从问题 2(线程池的工作原理)开始,因为它实际上是回答问题 1 的关键。线程池是作为(线程安全的)工作队列和组实现的(无需详细说明)工作线程(可以根据需要缩小或放大)。当用户调用QueueUserWorkItem时,任务被放入工作队列。工作人员继续轮询队列并在空闲时开始工作。一旦他们设法接受一项任务,他们就会执行它,然后返回队列进行更多工作(这非常重要!)。所以工作是由工人按需完成的:当工人变得空闲时,他们需要做更多的工作。

说了这么多,很容易看出问题 1 的答案是什么(为什么你会看到更细粒度的任务的性能差异):这是因为使用细粒度你可以获得更多的负载平衡(一个非常理想的属性) ,即您的工人或多或少地做相同数量的工作,并且所有核心都被统一利用。正如您所说,使用粗粒度的任务分布,可能会有更长和更短的任务,因此一个或多个核心可能会滞后,从而减慢整体计算速度,而其他核心则什么也不做。有了小任务,问题就消失了。每个工作线程一次执行一项小任务,然后返回执行更多任务。如果一个线程接一个较短的任务,它会更频繁地进入队列,如果它需要一个较长的任务,它会减少进入队列的频率,所以事情是平衡的.

最后,当作业太细粒度时,并且考虑到池可能会扩大到超过 1K 线程,当所有线程返回以执行更多工作时,队列上的争用非常高(这种情况经常发生),这可能会导致对于您看到的尖峰。如果底层实现使用阻塞锁来访问队列,那么上下文切换非常频繁,这会极大地损害性能并使其看起来相当随机。

于 2012-09-09T07:06:18.113 回答
0

问题1的答案:这是因为线程切换,线程切换(或操作系统概念中的上下文切换)是在每个线程之间切换所需的CPU时钟,大多数时候多线程会提高程序和进程的速度,但是当它是进程时如此小而快速的大小,那么上下文切换将比线程的自身进程花费更多的时间,因此整个程序的吞吐量会降低,您可以在 OS 概念书籍中找到有关此的更多信息。

问题 2 的答案:实际上我对 ThreadPool 有一个全面的了解,我无法准确解释它的结构。

于 2012-09-08T19:34:50.480 回答
0

要了解有关 ThreadPool 的更多信息,请从此处开始ThreadPool 类

每个版本的 .NET Framework 都增加了越来越多的间接利用 ThreadPool 的功能。例如之前提到的Parallel.ForEach 方法与System.Threading.Tasks一起添加到 .NET 4 中,这使代码更具可读性和整洁性。您也可以在此处了解更多关于任务计划程序的信息。

在非常基本的层面上,它所做的是:它创建了 20 个线程并将它们放入一个 lits 中。每次它接收到执行异步的委托时,它都会从列表中获取空闲线程并执行委托。如果没有找到可用的线程,则将其放入队列中。每次 deletegate 执行完成时,它都会检查队列是否有任何项目,如果有,就会偷看一个并在同一个线程中执行。

于 2012-09-08T20:31:06.020 回答