【问题标题】:How does the Parallel class dynamically adjust the level of parallelism?Parallel 类是如何动态调整并行度的?
【发布时间】:2019-12-02 13:39:24
【问题描述】:

TPL 使用什么反馈来动态调整工作线程的数量?

我之前的理解是,它衡量任务完成率,看看添加或删除线程是否值得。但是,为什么这段代码会不断增加线程数,即使信号量会引入瓶颈?

当然,每秒完成的任务不能超过 20 个,超过 2 个线程不会改善这一点。

var activeThreads = 0;
var semaphore = new SemaphoreSlim(2);
var res = Parallel.For(0, 1_000_000, i =>
{
    Interlocked.Increment(ref activeThreads);
    semaphore.Wait();
    try
    {
        Thread.Sleep(100);
        Console.WriteLine("Threads: " + activeThreads);
    }
    finally
    {
        Interlocked.Decrement(ref activeThreads);
        semaphore.Release();
    }
});

【问题讨论】:

  • @Liam 基本上,通过尝试添加另一个线程并测量任务完成率是否增加。如果它减少,放下一个线程。如果它增加,请尝试另一个。然后再次测量并重复。
  • 是的,它不会那样做。这将创建大量线程,但随后会阻塞除其中两个之外的所有线程。如果你只想要两个线程,你应该指定MaxDegreeOfParallelismParallel.For 并不像你想象的那么聪明。
  • But then, why does this code keep increasing the number of threads, even though there is a bottleneck introduced by a semaphore? 你写的东西很慢(从Parallel 的角度来看)。它无法看到为什么它很慢 - 但它知道您希望它并行完成。它不能让它运行得更快,所以它只能做它可以做的事情——在问题上抛出线程。
  • 这是一个不同的问题@relatively_random,也是一个常见的混淆点。线程池对于它在一个时间窗口内旋转多少线程是保守的。这就是为什么它不会一次旋转一百万个。撇开这样一个事实不谈,如果它这样做了,它就不会是一个。 :) 如果您想“加速”线程池的初始加速,请致电 docs.microsoft.com/en-us/dotnet/api/…
  • 我怀疑您的问题是您假设Parallel 具有智能(计算何时要求新的Tasks 等)。据我了解,Parallel 比较笨。 线程池 具有智能。 Parallel 可以请求任意数量,线程池 将控制它一次实际获取 的数量(以及它启动的新线程数量等)等等)。 我之所以提到这一点,是因为您关于为什么其他人错了的论点可能是您不清楚Parallel 的职责在哪里结束,而线程池的职责从哪里开始。

标签: c# .net task-parallel-library threadpool


【解决方案1】:

我相信 ParallelOptions 是您正在寻找的用于指定并行度的内容。

 Parallel.For(0, 1000, new ParallelOptions
        {
            MaxDegreeOfParallelism = 2
        }, i => { Console.WriteLine(i); });

就个人而言,我认为 TPL 库在很多情况下都可以使用,但它在执行分配方面并不是很聪明(请原谅我的英语)。每当您在应用程序的执行中遇到瓶颈时,请查看管道模式。这是一个链接,很好地描述了 imo 并行执行的不同方法:https://www.dotnetcurry.com/patterns-practices/1407/producer-consumer-pattern-dotnet-csharp

【讨论】:

    【解决方案2】:

    TL;DR:您在代码中所做的事情是阻塞,TPL 用来证明创建新线程是合理的。 (同步或休眠,或执行 I/O 都将被视为阻塞。)

    更长的解释...

    当你的任务运行时,它会占用它的线程 100 毫秒(因为你 Sleep(100))。在您睡眠时,该线程不能用于运行其他任务,因为当睡眠时间段到期时,它可能无法处于可运行状态。通常我们休眠而不是执行异步操作,因为我们需要保持我们的 调用堆栈 完整。因此,我们依靠堆栈来维护我们的状态。堆栈是线程的独一无二的资源。 (实际上,线程并没有比它的堆栈更多。)

    因此,TPL(特别是线程池)试图保持 占用率 较高,但线程数较低。它实现这一点的一种方法是确保系统中的可运行线程的数量与虚拟处理器的数量大致相同。每次需要增加线程数时,都必须为线程创建一个相对昂贵的stack,所以最好不要有这么多。并且无法调度不可运行的线程,因此当 CPU 空闲时,您需要进行调度以利用可用的处理资源。如果线程处于休眠状态,则无法安排它运行。所以取而代之的是,将一个线程添加到线程池中,并在其上安排下一个任务。

    当您编写可以由 TPL 分区和管理的这样的并行代码(如在 parallel for 循环中)时,您应该小心将线程置于不可运行状态。执行同步 I/O、等待同步对象(例如信号量、事件或互斥锁等)或休眠将使线程进入在 I/O 完成之前无法在线程上执行任何其他操作的状态,休眠间隔过期,或同步对象发出信号。这段时间线程对TPL没有好处。

    在你的情况下,你做了几件这样的事情:你等待一个信号量,你睡觉,你通过写控制台来执行 I/O。首先是等待那个信号量。如果没有发出信号,那么您会立即遇到线程不可运行的情况,并且需要运行的百万左右任务中的下一个任务必须安排在不同的线程上。如果没有,那么 TPL 可以证明创建一个新线程来启动更多任务是合理的。毕竟,如果线程#987,321 会真正结束设置信号量以解除阻塞任务#1 怎么办? TPL 不知道您的代码做了什么,因此本着效率的精神,它可以延迟创建线程一段时间,但为了正确性,最终它必须创建更多线程开始削减任务列表。有一个复杂的、特定于实现的启发式方法,它适用于监控、预测和以其他方式正确地猜测效率。

    现在您的具体问题实际上是询问它使用什么反馈来调整线程数。就像我说的,实际的实现很复杂,你可能应该把它想象成一个黑匣子。但简而言之,如果没有可运行的线程,它可能创建另一个线程来继续削减任务列表(或者可能等待一段时间再这样做,希望事情会腾出来),如果空闲线程过多,它会终止空闲线程以回收它们的资源。

    重申一下,正如我在顶部所说的,并希望这次能回答您的问题,您所做的一件事可以让 TPL 证明创建新线程是合理的,那就是阻止。 ...即使在第一个信号量上。

    【讨论】:

    • 我认为我是否阻止或旋转并不重要,但显然它确实如此。用 Interlocked.CompareExchange 循环替换信号量和睡眠,并且仅在最大增加似乎将其限制为 ~10 个线程时才写入控制台。
    【解决方案3】:

    在 2017 年遇到 article 分析线程注入算法。截至 2019-08-01,GitHub 上的 hillclimbing.cpp file 并没有真正改变,所以文章应该仍然是最新的。

    相关详情:

    .NET 线程池有两种主要的线程注入机制: 饥饿避免机制,如果看到没有则添加工作线程 在排队物品和爬山启发式方面取得了进展 尝试在使用尽可能少的线程的同时最大化吞吐量。

    ...

    它根据“当前”计算所需的线程数 吞吐量”,即“完成的任务数”(numCompletions) 在当前时间段内(sampleDuration 以秒为单位)。

    ...

    它还将当前线程计数(currentThreadCount)带入 考虑。

    ...

    真正的 .NET 线程池只会将线程数增加一 每 500 毫秒线程。它一直这样做,直到'# of 线程数已达到爬山算法的数量 建议。

    ...

    [爬山]算法仅返回符合限制的值 由 ThreadPool.SetMinThreads(..) 和 ThreadPool.SetMaxThreads(..)

    ...

    另外,【爬山算法】只会推荐 如果 CPU 利用率低于 95%,则增加线程数

    原来线程池确实有基于任务完成率的反馈机制。它也没有明确检查其线程是否被阻塞或正在运行,但它会密切关注整体 CPU 利用率以检测阻塞。这一切也意味着它应该大致了解其他线程和进程在做什么。

    另一方面,它总是会急切地产生至少与ThreadPool.SetMinThreads() 所告知的一样多的线程,默认为机器上逻辑处理器的数量。

    总之,有问题的测试代码做了两件事,导致它不断堆积更多线程:

    • 有很多任务排队等待很长时间,这表明饥饿
    • CPU 利用率可以忽略不计,这意味着新线程应该能够使用它

    【讨论】:

      猜你喜欢
      • 2021-09-22
      • 1970-01-01
      • 2011-03-05
      • 1970-01-01
      • 1970-01-01
      • 2021-12-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多