C#ThreadPool实现/性能峰值

时间:2012-09-08 19:16:30

标签: c# multithreading threadpool

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

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

我注意到的第一件事是表现很疯狂。平均后,线程池似乎更快一点,但性能上有大量峰值(每次更新大约10 ms,随机跳转到40-60ms)。我尝试使用ANTS对此进行分析,但是我无法深入了解尖峰发生的原因。

我的下一个方法是仍然使用ThreadPool,但是我将所有对象拆分成组。我最初只开始使用8组,因为这是我的计算机所拥有的核心。表现很棒。它远远超过单线程方法,并且没有尖峰(每次更新大约6ms)。

我唯一想到的是,如果一项工作在其他工作之前完成,那么就会有一个空闲核心。因此,我将作业数量增加到大约20个,甚至达到500个。正如我预期的那样,它下降到5毫秒。

所以我的问题如下:

  • 为什么当我的工作规模快/多时会出现尖峰?
  • 有没有深入了解如何实现ThreadPool,以帮助我理解如何最好地使用它?

5 个答案:

答案 0 :(得分:4)

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

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

答案 1 :(得分:3)

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

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

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

Parallel.ForEach(physicObjects, physicObject => Process(physicObject));

答案 2 :(得分:2)

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

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

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

最后,当作业太细粒度,并且考虑到池可能扩大到1K以上的线程时,当所有线程返回需要更多工作时(这种情况经常发生),队列上存在非常高的争用,这可能是你所看到的峰值的原因。如果底层实现使用阻塞锁来访问队列,那么上下文切换非常频繁,会严重损害性能并使其看起来相当随机。

答案 3 :(得分:0)

回答问题1: 这是因为线程切换,线程切换(或OS概念中的上下文切换)是在每个线程之间切换的CPU时钟,大多数情况下多线程会提高程序和进程的速度,但是当它的进程如此小而且快速时大小然后上下文切换将比线程的自我处理花费更多的时间,因此整个程序吞吐量减少,您可以在操作系统概念书中找到更多相关信息。

回答问题2: 实际上我对ThreadPool有一个全面的了解,我无法解释它的结构究竟是什么。

答案 4 :(得分:0)

了解更多有关ThreadPool的信息,请点击此处ThreadPool Class

每个版本的.NET Framework都间接地利用ThreadPool添加了越来越多的功能。比如之前提到的Parallel.ForEach Method在.NET 4中添加了System.Threading.Tasks,这使代码更具可读性和整洁性。您也可以在Task Schedulers了解更多信息。

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