为了加速C#中物理对象的处理,我决定将线性更新算法改为并行算法。我认为最好的方法是使用ThreadPool,因为它是为完成一个工作队列而构建的。
当我第一次实现并行算法时,我为每个物理对象排队了一份工作。请记住,单个作业相当快速地完成(更新力,速度,位置,检查与周围任何对象的旧状态的碰撞,以使其线程安全等)。然后,我将等待所有作业使用单个等待句柄完成,并使用一个互锁整数,每次物理对象完成时我都会递减(当达到零时,我会设置等待句柄)。等待是我需要做的下一个任务,需要更新所有对象。
我注意到的第一件事是表现很疯狂。平均后,线程池似乎更快一点,但性能上有大量峰值(每次更新大约10 ms,随机跳转到40-60ms)。我尝试使用ANTS对此进行分析,但是我无法深入了解尖峰发生的原因。
我的下一个方法是仍然使用ThreadPool,但是我将所有对象拆分成组。我最初只开始使用8组,因为这是我的计算机所拥有的核心。表现很棒。它远远超过单线程方法,并且没有尖峰(每次更新大约6ms)。
我唯一想到的是,如果一项工作在其他工作之前完成,那么就会有一个空闲核心。因此,我将作业数量增加到大约20个,甚至达到500个。正如我预期的那样,它下降到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)
答案 4 :(得分:0)
了解更多有关ThreadPool的信息,请点击此处ThreadPool Class
每个版本的.NET Framework都间接地利用ThreadPool添加了越来越多的功能。比如之前提到的Parallel.ForEach Method在.NET 4中添加了System.Threading.Tasks,这使代码更具可读性和整洁性。您也可以在Task Schedulers了解更多信息。
在非常基本的层面上它的作用是:它创建让我们说20个线程并将它们放入lits中。每次收到委托执行异步时,它都会从列表中获取空闲线程并执行委托。如果找不到可用的线程,则将其放入队列中。每次deletegate执行完成时,它将检查队列是否有任何项目,如果是这样,则查看一个并在同一个线程中执行。