14

特别是,我正在考虑使用 TPL 来启动(并等待)外部进程。在决定开始另一个任务(因此——在我的情况下——另一个外部进程)之前,TPL 是否会查看总机器负载(CPU 和 I/O)?

例如:

我有大约 100 个需要编码或转码的媒体文件(例如从 WAV 到 FLAC 或从 FLAC 到 MP3)。编码是通过启动一个外部进程(例如 FLAC.EXE 或 LAME.EXE)来完成的。每个文件大约需要 30 秒。每个进程主要受 CPU 限制,但其中有一些 I/O。我有 4 个内核,所以最坏的情况(通过管道将解码器转码到编码器)仍然只使用 2 个内核。我想做类似的事情:

Parallel.ForEach(sourceFiles,
    sourceFile =>
        TranscodeUsingPipedExternalProcesses(sourceFile));

这会启动 100 个任务(因此有 200 个外部进程竞争 CPU)吗?或者它会看到 CPU 很忙,一次只做 2-3 个?

4

3 回答 3

22

你会在这里遇到几个问题。调度程序的饥饿避免机制将看到您的任务在等待进程时被阻塞。它将发现很难区分死锁线程和只是等待进程完成的线程。因此,如果您的任务运行或长时间运行(见下文),它可能会安排新任务。爬山启发式应该考虑到系统上的整体负载,包括您的应用程序和其他应用程序。它只是试图最大化完成的工作,所以它会增加更多的工作,直到系统的整体吞吐量停止增加,然后它会回退。我认为这不会影响您的申请,但可能会避免饥饿问题。

您可以在Parallel Programming with Microsoft®.NET、Colin Campbell、Ralph Johnson、Ade Miller、Stephen Toub 中找到有关这一切如何工作的更多详细信息(早期的草稿在线)。

“.NET 线程池自动管理池中工作线程的数量。它根据内置的启发式方法添加和删除线程。.NET 线程池有两种主要的线程注入机制:增加工作线程的饥饿避免机制如果它发现排队的项目没有任何进展,则线程和爬山启发式尝试在使用尽可能少的线程的同时最大化吞吐量。

饥饿避免的目标是防止死锁。当工作线程等待同步事件时,可能会发生这种死锁,而该同步事件只能由仍在线程池的全局或本地队列中挂起的工作项来满足。如果有固定数量的工作线程,并且所有这些线程都被类似地阻塞,那么系统将无法取得进一步的进展。添加一个新的工作线程解决了这个问题。

爬山启发式的一个目标是在线程被 I/O 或其他使处理器停止的等待条件阻塞时提高内核的利用率。默认情况下,托管线程池每个核心有一个工作线程。如果这些工作线程中的一个被阻塞,则内核可能未被充分利用,具体取决于计算机的整体工作负载。线程注入逻辑不区分被阻塞的线程和正在执行冗长的处理器密集型操作的线程。因此,只要线程池的全局或本地队列包含待处理的工作项,运行时间较长(超过半秒)的活动工作项就会触发创建新的线程池工作线程。

每次工作项完成或以 500 毫秒间隔(以较短者为准)时,.NET 线程池都有机会注入线程。线程池利用这个机会尝试添加线程(或将它们移除),由线程计数先前更改的反馈引导。如果添加线程似乎有助于提高吞吐量,那么线程池会增加更多;否则,它会减少工作线程的数量。这种技术被称为爬山启发式。因此,保持单个任务简短的一个原因是避免“饥饿检测”,但保持简短的另一个原因是通过调整线程数为线程池提供更多提高吞吐量的机会。单个任务的持续时间越短,线程池就越频繁地测量吞吐量并相应地调整线程数。

为了具体化,考虑一个极端的例子。假设您有一个复杂的财务模拟,其中包含 500 个处理器密集型操作,每个操作平均需要十分钟才能完成。如果您在全局队列中为这些操作中的每一个创建顶级任务,您会发现大约五分钟后线程池将增长到 500 个工作线程。原因是线程池将所有任务都视为阻塞,并开始以大约每秒两个线程的速度添加新线程。

500 个工作线程有什么问题?原则上,如果您有 500 个内核供他们使用,并且有大量系统内存,则没什么。事实上,这是并行计算的长期愿景。但是,如果您的计算机上没有那么多内核,那么您将处于许多线程竞争时间片的情况。这种情况称为处理器超额订阅。允许许多处理器密集型线程在单个内核上竞争时间会增加上下文切换开销,从而严重降低整体系统吞吐量。即使您没有耗尽内存,在这种情况下的性能也可能比顺序计算差得多。(每个上下文切换需要 6,000 到 8,000 个处理器周期。)上下文切换的成本并不是开销的唯一来源。中的托管线程。NET 大约消耗一兆字节的堆栈空间,无论该空间是否用于当前执行的函数。创建一个新线程大约需要 200,000 个 CPU 周期,而退休一个线程大约需要 100,000 个周期。这些都是昂贵的操作。

只要你的任务不花几分钟,线程池的爬山算法最终会意识到它有太多的线程并自行削减。但是,如果您确实有任务占用工作线程数秒、数分钟或数小时,那将摆脱线程池的启发式算法,此时您应该考虑替代方案。

第一个选项是将您的应用程序分解为较短的任务,这些任务完成的速度足够快,以便线程池成功控制线程数以获得最佳吞吐量。第二种可能性是实现您自己的不执行线程注入的任务调度程序对象。如果您的任务持续时间较长,则不需要高度优化的任务调度程序,因为与任务的执行时间相比,调度成本可以忽略不计。MSDN® 开发人员程序有一个简单的任务调度程序实现示例,它限制了最大并发度。有关详细信息,请参阅本章末尾的“进一步阅读”部分。

作为最后的手段,您可以使用 SetMaxThreads 方法为 ThreadPool 类配置工作线程数的上限,通常等于内核数(这是 Environment.ProcessorCount 属性)。此上限适用于整个流程,包括所有 AppDomain。”

于 2010-08-15T19:26:22.493 回答
2

最简洁的答案是不。

在内部,TPL 使用该标准ThreadPool来安排其任务。因此,您实际上是在询问是否ThreadPool考虑了机器负载而没有考虑。唯一限制同时运行的任务数量是线程池中的线程数,仅此而已。

外部流程准备好后是否可以向您的应用程序报告?在这种情况下,您不必等待它们(保持线程被占用)。

于 2010-08-15T19:27:21.437 回答
-1

使用 TPL/ThreadPool 运行测试以安排大量任务执行循环自旋。使用外部应用程序,我已使用 proc 亲和力将其中一个核心加载到 100%。活动任务的数量从未减少。

更好的是,我运行了同一个 CPU 密集型 .NET TPL 启用应用程序的多个实例。所有应用程序的线程数都是相同的,并且从未低于内核数,即使我的机器几乎无法使用。

所以抛开理论不谈,TPL 使用可用的核心数量,但从不检查它们的实际负载。在我看来,这是一个非常糟糕的实施。

于 2013-05-14T17:06:16.107 回答