【问题标题】:ThreadPool callbacks in tight loop - 100% CPU紧密循环中的线程池回调 - 100% CPU
【发布时间】:2012-04-25 23:01:36
【问题描述】:

我的算法中有一个方法可以在非常大的数据集上运行非常紧密的循环。我最初是单线程写的,这很好,但是花了很长时间。我现在想要加快速度,所以我现在使用 ThreadPool 来并行化工作。问题是这会导致我的 CPU 使用率达到 95-100%,这是我所预料的。然而,我的表现已经显着提高,但我认为如果我可以减少所有上下文切换,我可以做得更好。这也导致我的其他程序有点滞后,因为它们必须与线程争夺 CPU 资源。

我的问题是我应该怎么做?我唯一能想到的就是限制一次运行的线程数,但这可能会使我的算法变慢,因为一次只能运行几个线程。我也不想在我的线程中添加睡眠,因为我只需要算法尽快运行完成。

编辑:有几个人提到过使用 TPL。我认为这是个好主意,但不幸的是,我忘了提到我一直在使用 .NET 3.5,因为父应用程序尚未发布使用 .NET 4 的版本。

【问题讨论】:

  • 如果你想要速度,为什么你会拿出所有超速的东西?上下文切换是由操作系统进行的,你不要乱用......
  • 解决办法是降低池中线程的优先级。这不是一个答案,因为我不知道如何有效地做到这一点:(
  • @Martin: SetPriorityClass 将是降低线程池优先级以支持其他应用程序的正确方法。
  • 我不明白。大概您有多个内核,并且通过并行化您的代码来提高性能。由于所有内核都在忙于运行您的应用程序,因此拥有 100% 的 CPU 利用率似乎表明您已经成功了。
  • @natephette:使用SetPriorityClass 降低整个应用程序的优先级,让它只使用浏览器不需要的CPU。 .NET 中的 Process.PriorityClass 属性应该可以帮助您解决这个问题。

标签: c# cpu threadpool


【解决方案1】:

这都是关于资源管理的。您的程序当前占用了所有资源,因此其他程序对它们的访问权限减少。您需要平衡“我只需要算法尽快完成”部分与“这也导致我的其他程序有点滞后,因为它们必须与线程争夺 CPU 资源”。它们是相互排斥的;您不能让您的应用程序在特定机器上尽可能快地运行,同时也让其他应用程序保持完美响应。 CPU 在任何时间段内可以做的事情都是有限度的。

就效率提升而言,您可以做以下几件事:

  • 不要将 ThreadPool 用于超优化线程算法。 ThreadPool 非常适合简单的“开始执行此操作并让我知道您已完成”操作。但是,如果您希望进行优化,则可以避免使用 ThreadPool 添加额外级别的线程调度所固有的开销(在 CPU 和操作系统固有的开销之上)。您还可以对 ThreadPool 中的线程进行更有限的控制,这意味着诸如分配处理器亲和性(负载平衡)和优先级(给线程更多或更少的时间)之类的优化不可用。尝试创建简单的线程,或研究 TPL,它有多种策略来完成多项任务(并非所有这些都首先需要线程)。

  • 是的,您会希望能够“限制”线程数。这既是为了通过减少程序对它的需求来允许其他程序一些 CPU 时间,但正如我所说,多线程也存在固有的开销。经验法则是,如果 CPU 具有两倍以上的主动运行线程数,因为它具有“执行单元”(这些是 CPU 芯片上的物理内核,以及像超线程技术那样拆分一个内核的“逻辑处理器”)成两个),那么操作系统将花费更多的时间来调度线程并在它们之间切换(“缓存抖动”),而不是实际运行线程所花费的时间。更一般地说,存在收益递减规律,这将发展为“规模不经济”;最终,添加另一个线程将导致您的程序运行得比您没有使用该线程时更慢。是的,ThreadPool 可以为您处理最大线程数,但这可能是您在自己的算法中实现的各种功能中最简单的。

  • 确保优化每个线程的工作。寻找幼稚或低效的算法(我称它们为“O(我的上帝)-复杂性”)并简化它们。大多数操作的效率都有一个下限(因操作类型而异),“过早优化是万恶之源”(不要以牺牲代码实际工作为代价来优化性能),但是请理解,在多线程环境中,您在运行一次算法时可以提高算法效率的任何收益都将乘以您运行它的次数,因此确保并行操作的效率是双重奖励。

【讨论】:

  • '经验法则是,如果一个 CPU 的活跃运行线程数增加一倍以上,因为它具有“执行单元”(这些是 CPU 芯片上的物理内核,并且“逻辑处理器”,例如将一个内核分成两个的超线程技术),那么操作系统将花费更多的时间来调度线程并在它们之间切换(“缓存抖动”)而不是实际运行线程所花费的时间——你真的尝试过吗?在非托管代码上,您有 8 个 CPU 绑定线程还是 800 个线程没有任何区别 - 完成的工作量大致相同。
  • 那么如果我有一个核心 i7 CPU(4 个物理核心 + 4 个虚拟核心),根据该规则,16 个线程是限制?
  • 我预计 16 个线程将是您开始看到显着递减收益的点。由于其他程序和操作系统也需要线程,您可能会看到比这更早的下降;玩弄几个上限线程数限制和一个秒表来为算法计时。
  • 在托管或非托管代码上,在 200 个线程之后收益不会递减,除非资源(即 RAM)很短,并且额外的堆栈将您的应用程序工作集推到了分页的边缘。我刚刚在 C# 上对此进行了测试,所以我没有全面的结果,是的,但是我的 i7 上的 200 个准备好的托管线程实际上设法比 16 个略快地完成 CPU 密集型任务。
  • 只是想把我选择的方法和结果告诉大家。我从使用 ThreadPool 切换到仅使用简单线程。我将它们全部设置为低于正常优先级。我的 CPU 仍然保持在 100%,但我的程序确实保持响应。我继续在一个小数据集上运行了一些基准测试。在所有线程处于正常优先级的情况下,我得到了 3.935 秒。在低于正常优先级时,我得到了 5.92 秒。我知道这并不能说明全部情况,但它仍然比我在运行单线程时得到的 725.6 秒要好。
【解决方案2】:

如果您可以将主应用程序重写为 foreach 循环而不是 IEnumerable,您可以使用 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;
  }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-12-16
    • 1970-01-01
    • 1970-01-01
    • 2011-01-13
    • 1970-01-01
    • 2010-11-03
    • 2010-11-16
    相关资源
    最近更新 更多