代码之家  ›  专栏  ›  技术社区  ›  Roger Lipscombe

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

  •  14
  • Roger Lipscombe  · 技术社区  · 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?

    3 回复  |  直到 11 年前
        1
  •  21
  •   Ade Miller    14 年前

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

    你可以找到更多关于这一切如何运作的细节 Parallel Programming with Microsoft®.NET ,科林·坎贝尔,拉尔夫·约翰逊,艾德·米勒,斯蒂芬·图布(之前的草稿是 online )

    “.NET线程池自动管理工作线程数 池中的线程。它根据内置的 试探法。.NET线程池有两种主要的注入机制 线程:一种避免饥饿的机制,增加了工人 线程如果在排队的项目和爬坡上看不到进展 尝试在使用as时最大化吞吐量的启发式方法 螺纹尽可能少。

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

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

    .NET线程池有机会在 工作项完成的时间或以500毫秒的间隔,以两者中的哪一个为准 较短。线程池使用此机会尝试添加线程 (或将其带走),由之前在 线程计数。如果添加线程似乎有助于提高吞吐量, 线程池增加了更多;否则,它会减少 工作线程。这种技术被称为爬山启发式。 因此,缩短单个任务的一个原因是避免 饥饿检测,但另一个保持其简短的原因是 给线程池更多的机会通过 调整螺纹数量。个体持续时间越短 任务,线程池可以更频繁地测量吞吐量和 相应地调整线数。

    为了具体化,考虑一个极端的例子。假设 您有一个复杂的财务模拟,需要500个处理器 操作,每个操作平均需要10分钟 完成。如果在全局队列中为每个 在这些操作中,大约五分钟后你会发现 线程池将增长到500个工作线程。原因是 线程池将所有任务视为已阻止,并开始添加新任务 线程的速率大约为每秒两个线程。

    500个工作线程有什么问题?原则上,如果 你有500个核心供他们使用,还有大量的系统 记忆。实际上,这是并行计算的长期远景。 但是,如果您的计算机上没有那么多的核心,那么 在许多线程都在争夺时间片的情况下。这个 这种情况称为处理器过度订阅。允许许多 处理器密集型线程在单个核心上竞争时间增加 上下文切换开销,可严重减少整个系统 吞吐量。即使你没有用完内存,在这个性能 情况可能比顺序计算严重得多。 (每个上下文切换需要6000到8000个处理器周期。) 上下文切换的成本并不是开销的唯一来源。 .NET中的托管线程大约消耗1兆字节的堆栈 空间,无论该空间是否用于当前执行的函数。 创建新线程大约需要200000个CPU周期,并且 大约100000个周期使线程失效。这些是昂贵的操作。

    只要你的任务不需要每分钟,线程池 爬山算法最终会发现它有太多的线程 自动削减开支。但是,如果您的任务 占用工作线程许多秒、分钟或小时,即 将抛出线程池启发式方法,此时您 应该考虑另一种选择。

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

    最后,您可以使用setmaxthreads方法 用数字的上限配置threadpool类 工作线程数,通常等于核心数(这是 environment.processorCount属性)。此上限适用于 整个过程,包括所有AppDomain。“

        2
  •  2
  •   Ronald Wildenberg    14 年前

    简短的回答是:不。

    在内部,TPL使用标准 ThreadPool 安排任务。所以你实际上是在问 线程池 考虑到机器负载,它没有考虑到。唯一限制同时运行的任务数的是线程池中的线程数,而不是别的。

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

        3
  •  -1
  •   MoonStom    11 年前

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

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

    所以理论上,TPL使用可用的内核数量,但从不检查它们的实际负载。我认为这是一个非常糟糕的实施。