task-parallel-library - 任务并行库(或 PLINQ)是否考虑了其他进程?

标签 task-parallel-library

特别是,我正在考虑使用 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个?

最佳答案

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

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

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

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

爬山启发式的一个目标是提高利用率
当线程被 I/O 或其他等待条件阻塞时的内核数
停止处理器。默认情况下,托管线程池有一个
每个内核的工作线程。如果这些工作线程之一变为
被阻止,核心可能未得到充分利用,具体取决于
计算机的整体工作负载。线程注入(inject)逻辑
不区分阻塞的线程和线程
这正在执行一个冗长的、处理器密集型的操作。所以,
每当线程池的全局或本地队列包含挂起
工作项,需要很长时间才能运行的事件工作项(超过
半秒)可以触发创建新的线程池worker
线程。

.NET 线程池有机会每次注入(inject)线程
工作项完成的时间或以 500 毫秒为间隔,以哪个为准
更短。线程池利用这个机会尝试添加线程
(或将它们带走),以先前更改的反馈为指导
线程数。如果添加线程似乎有助于提高吞吐量,
线程池增加更多;否则,它会减少数量
工作线程。这种技术称为爬山启发式。
因此,保持单个任务简短的原因之一是避免
“饥饿检测”,但另一个保持简短的原因是
给线程池更多的机会来提高吞吐量
调整线程数。个人的持续时间越短
任务,线程池可以测量吞吐量和
相应地调整线程数。

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

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

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

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

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

关于task-parallel-library - 任务并行库(或 PLINQ)是否考虑了其他进程?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/3488381/

相关文章:

c# - 如何通过局部变量获取线程结果?

c# - 说明 async/await 和 Reactive (Rx) 扩展范例之间区别的代码示例?

c# - 为什么原来的任务在 ContinueWith else 时被取消了?

C# 暂停异步任务适用于一种方法,但不适用于另一种方法

c# - C#中的多个并发定期刷新操作

c# - 异步EF 6.0比同步花费的时间更长

c# - 如何等到一系列任务完成?

c# - 将包含大量 "locks"的代码重构为更多无锁代码

.net - 我怎样才能让一个 IPropagatorBlock<TInput, TOutput> 自动停止?

c# - FIle.ReadAll***Async/WriteAll***Async/AppendAll***Async 方法在哪里?