线程池回调在紧密循环中 - 100% CPU

5
我在算法中有一个方法,它在非常大的数据集上运行一个非常紧密的循环。我最初编写了单线程版本,效果还不错,但是需要花费很长时间。现在我想加速它,所以使用ThreadPool并行化工作。问题是这会导致我的CPU使用率达到95-100%,我有点意料之中。然而,性能已经显著提高,但我认为如果减少所有上下文切换,性能可能会更好。这也使我的其他程序有点卡顿,因为它们必须与线程争夺CPU资源。
我的问题是我该如何解决这个问题?我唯一能想到的是限制同时运行的线程数量,但这可能会使我的算法变慢,因为只有很少的线程能同时运行。我也不想在我的线程中添加sleeps,因为我只需要尽快完成算法运行。
编辑:几个人提到了使用TPL。我认为这是一个很好的想法,但不幸的是,我忘记提到我被困在使用.NET 3.5的情况下,因为父应用程序尚未发布使用.NET 4的版本。

1
如果你想要速度,为什么要去除所有加速的东西?上下文切换是由操作系统完成的,你不要去干扰它... - gbianchi
1
解决方案是降低线程池中线程的优先级。这不是一个答案,因为我不知道如何高效地做到这一点 :( - Martin James
2
@Martin:SetPriorityClass是降低线程池优先级以支持其他应用程序的正确方法。 - Ben Voigt
2
我不明白。你可能有多个核心,通过并行化你的代码,你想提高性能。因为所有核心都在运行你的应用程序而导致CPU利用率达到100%似乎表明你已经成功了。 - Martin Liversage
3
使用SetPriorityClass函数降低整个应用程序的优先级,让它只使用浏览器不需要的CPU资源。在.NET中,Process.PriorityClass属性可以帮助你实现此功能。 - Ben Voigt
显示剩余3条评论
2个回答

6
这篇文章主要是关于资源管理的。你的程序目前正在占用所有资源,因此其他程序的访问量减少了。你需要平衡“我只需要算法尽快运行完毕”的部分和“这也会导致我的其他程序有点卡顿,因为它们必须与线程争夺CPU资源”的部分。它们是互相排斥的;你不能让你的应用在特定的机器上尽可能快地运行,同时保持其他应用程序完全响应。在任何一段时间内,CPU能做的事情都是有限的。
就效率提高而言,你可以做以下几件事情:
  • 不要为超优化的多线程算法使用线程池。线程池非常适合简单的“去执行这个任务并让我知道你完成了”的操作。然而,如果你想进行优化,那么线程池中添加额外的线程调度层所固有的开销(加上CPU和操作系统固有的开销)就可以避免。你对线程的控制也更加有限,意味着像分配处理器亲和力(以平衡负载)和优先级(给予线程更多或更少的时间)等优化方法对于线程池中的单个线程不可用。尝试创建简单的线程,或者研究TPL,它有许多策略可以完成多个任务(其中不是所有的任务都需要首先进行线程处理)。

  • 是的,你会想要能够“节流”线程的数量。这既是为了通过减少你的程序对CPU的需求来允许其他程序获得一些CPU时间,但正如我所说的,多线程固有的开销也是不可避免的。经验法则是,如果一个CPU被赋予比其“执行单元”(这些是CPU芯片上的物理核心和“逻辑处理器”,例如将一个核心分成两个的超线程技术)更多的正在运行的线程数的两倍以上,则操作系统将花费更多的时间调度线程和在它们之间切换(“缓存抖动”),而不是实际运行线程。更一般地说,存在着收益递减的规律,这将进展到“规模不经济”;最终,添加另一个线程将使你的程序比没有使用该线程时运行得更慢。是的,线程池会为你处理最大线程数,但那可能是它各种功能中最简单的一项,你可以在自己的算法中实现它。

  • 确保每个线程的工作都被优化。寻找天真或低效的算法(我称之为“O(我的天)-复杂度”)并简化它们。大多数操作的效率都有下限(这取决于操作类型),而“过早地进行优化是万恶之源”(不要以牺牲使代码实际工作为代价来优化性能),但在多线程环境中,你可以通过运行一次算法时的效率提高来乘以你运行它的次数,因此确保并行操作是有效的是一个双重加分。


1
“经验法则是,如果一个CPU的运行线程数超过其“执行单元”的两倍(这些是CPU芯片上的物理核心和“逻辑处理器”(如HyperThreading技术将一个核心分成两个)),那么操作系统将花费更多时间调度线程并在它们之间切换(“缓存抖动”),而不是实际运行线程。” - 你是否真的尝试过这样做?对于非托管代码,无论您有8个CPU绑定线程还是800个,大致完成相同数量的工作。 - Martin James
那么如果我有一颗Core i7 CPU(4个物理核心+4个虚拟核心),根据这个规则,16个线程是极限吗? - Nathan Phetteplace
我预计16个线程是你开始看到显著收益递减的点。由于其他程序和操作系统也需要线程,您可能会比这更早地看到下降;尝试使用多个上限线程计数和秒表来计时算法。 - KeithS
在托管代码或非托管代码中,除非资源(例如RAM)不足并且额外的堆栈将您的应用程序工作集推到分页边缘,否则在200个线程之后没有递减的回报。我刚在C#上测试过,所以我还没有全面的结果,但是在我的i7上,200个就绪的托管线程实际上比16个线程更快地完成了CPU密集型任务。 - Martin James
只是想向大家介绍我选择的方法和结果。我从使用线程池切换到使用简单线程。我将它们全部设置为BelowNormal优先级。我的CPU仍然被占用了100%,但我的程序仍然保持响应。我在一个小数据集上进行了一些基准测试。所有线程都设置为Normal优先级时,我得到了3.935秒的时间。在BelowNormal优先级下,我得到了5.92秒的时间。我知道这并不能说明全部问题,但这仍然比单线程运行时的725.6秒要好。 - Nathan Phetteplace

2
如果您可以将主应用程序重写为在 IEnumerable 上的 foreach 循环,那么您可以使用PLINQ来并行化您的循环。然后,您可以使用 WithDegreeOfParallelism 来控制应用程序将使用多少个内核。通过不使用计算机上的所有内核,您可以避免一些您经历的“延迟”。此外,您也不必处理如何跨线程分区循环以避免不必要的资源争用。 PLINQ 会为您完成所有这些工作。
假设您有此非常简单的单线程循环:
var arrayOfStuff = new[] { ... };
for (var i = 0; i < arrayOfStuff.Length; ++i)
  DoSomething(arrayOfStuff[i]);

如果顺序不重要,您可以使用 PLINQ 并行化它,使用比可用核心少一个的核心:
var cores = Math.Max(1, Environment.ProcessorCount - 1);
arrayOfStuff.AsParallel().WithDegreeOfParallelism(cores).ForAll(DoSomething);

即使你的主循环更加复杂,你也可以将其重写为一个迭代器块,然后对其进行并行化处理:
IEnumerable<Stuff> GetStuff() {
  for ( ... very complex looping ... ) {
    ...
    yield return stuff;
  }
}

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