【问题标题】:TPL with LongRunning state and thread synchronization and performance具有 LongRunning 状态和线程同步和性能的 TPL
【发布时间】:2014-07-23 08:19:45
【问题描述】:

我对在 LongRunning 状态下使用 TPL 有一个疑问。

来自 MSDN TPL 的目的是通过简化向应用程序添加并行性和并发性的过程来提高开发人员的工作效率。 TPL 动态调整并发程度,以最有效地使用所有可用的处理器内核。 TPL 的另一个好处是,您不必处理线程创建和同步.

但是如果我设置了 LongRunning 选项,TPL 会从线程池之外分配一个专用线程。因此,在这种情况下,它的工作原理类似于传统的线程(我相信,如果我错了,请更正)。那么在这样的场景下,TPL 本身会不会像上面提到的那样处理线程创建和同步呢?它还会自动/内部动态扩展并发程度,以最有效地使用所有处理器内核或开发人员需要编写代码来处理所有这些吗?

【问题讨论】:

  • 粗体声明适用于Parallel.For TPL API 系列。它不适用于Task.RunTask.Factory.StartNew,您可以明确控制并行度。
  • @Noseratio: 默认 Run(f)/StartNew(f) 不是在线程池上执行任务吗?我的意思是,没有手动控制选项。如果是这样,它已经扩大到我认为的可用 procs 数量?

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


【解决方案1】:

TPL 将并发程度动态调整为最大 有效地使用所有可用的处理器内核。其他 TPL 的好处是,您不必处理线程 创建和同步。

此声明适用于Parallel.For 系列 TPL API。它不适用于Task.RunTask.Factory.StartNew,您可以明确控制并行度。

对于Task.Run(以及带有默认选项的Task.Factory.StartNew),没有智能“缩放”。这只是简单的循环执行工作项,就像ThreadPool.QueueUserWorkItem 一样。这实际上可能会占用所有可用的池线程(最多ThreadPool.GetMaxThreads),然后在繁忙的池线程可用时将新任务排队等待延迟执行。也可能是the thread pool stuttering issue的主题。

使用Task.Factory.StartNewLongRunning 不同之处仅在于您可以避免线程池卡顿问题,但最终您可能会简单地耗尽操作系统内存和其他资源,因为操作系统线程是一种非常昂贵的资源。

Parallel.For 等情况下,TPL 调度程序更加智能。它不会在每个工作项一个线程的基础上浪费线程。相反,它具有相当复杂的命令式逻辑,考虑到 CPU/内核的数量以及可能的其他一些运行时指标。

更新以解决评论,这里有一个简单的例子:

using System;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication
{
    class Program
    {
        static void Main(string[] args)
        {
            int max = 50;
            int delay = 30; // ~30s per work item

            ThreadPool.SetMaxThreads(max, max);

            Console.WriteLine("starting, threads: {0}", Process.GetCurrentProcess().Threads.Count);

            var tasks = Enumerable.Range(0, max).Select(n => Task.Factory.StartNew(() =>
            {
                Console.WriteLine("task: {0}, threads: {1}, pool thread: {2}", 
                    n, Process.GetCurrentProcess().Threads.Count, Thread.CurrentThread.IsThreadPoolThread);

                for (int i = 0; i < delay * 1000; i++)
                {
                    Thread.Sleep(1);
                }
            })).ToArray();

            Console.WriteLine("waiting, threads: {0}", Process.GetCurrentProcess().Threads.Count);
            Task.WaitAll(tasks);

            Console.WriteLine("done, threads: {0}", Process.GetCurrentProcess().Threads.Count);
            Console.ReadLine();
        }
    }
}

输出(发布版本,未附加调试器,.NET 4.5,4 核 CPU):

开始,线程:3 任务:0,线程:11,池线程:True 任务:2,线程:11,池线程:True 等待,线程:11 任务:1,线程:11,池线程:真 任务:3,线程:11,池线程:True ... 任务:48,线程:56,池线程:真 任务:49,线程:57,池线程:True 完成,线程数:47

它确认了ThreadPool 的增长和卡顿行为,最多可达max 线程数。新线程的创建延迟约 500 毫秒。

现在,如果我们将TaskCreationOptions.LongRunning 添加到Task.Factory.StartNew,我们消除了口吃,并且我们不再受ThreadPool 大小的限制,但我们最终仍将参与到max 数字新线程,每个任务一个(取决于每个工作项执行的时间)。

它还会自动/内部调整并发程度 动态地最有效地使用所有处理器内核或 开发者需要编写代码来处理所有这些吗?

因此,如果开发人员想要使用 TPL 的 Task.RunTask.Factory.StartNew API,他或她确实需要手动处理并行级别。不过这并不难,例如,SemaphoreSlim

【讨论】:

  • 那篇博文来自 2006 年,请查看 danielmoth.com/Blog/… 来自 2008 年的关于 .Net4 的信息。仅供参考,我不认为你在这里错了。线程池仍然会被饿死。
  • @quetzalcoatl,我的解释似乎是正确的,请检查更新。
  • 我想说的是,如果 TPL 配置正确(LongRunning 等),手动管理池大小是没有意义的。它旨在处理许多小型工作,但如果您的代码中存在开始使其挨饿的错误,它仍然可以适应关键情况。许多长作业不应排队到默认线程池中。他们应该有自己的线程,或者,如果您需要限制它,那么您应该创建自己的额外线程池并将较长的任务分配给它。这就是存在 LongRunning 选项的原因,也是 TPL 接受不同调度程序的原因。它不会让 Run/StartNew 变得更糟。
【解决方案2】:

在下面的某个地方总是有“传统线程”。

本机线程“很重”。如果您为每个任务执行一个线程,然后创建非常多的任务(因此是线程),那么您可能会使进程(甚至整个机器或某些系统)饿死/停止。这使得注册许多微小的操作并以这种方式处理变得不可能/不可行,并且该障碍会影响您的代码架构。

这就是线程池的用武之地。改变想法,不为每个作业运行一个线程,而是让一些线程池化并让它们在共享任务队列上工作,将线程数量限制为恰好池的 N,并且您可以获得后台消息处理的好处。

草拟这个想法,重要的是要注意线程池(通常)仅限于一些 N 个线程。这意味着如果您将许多长时间运行的任务注册到线程池,您可能会饿死它。如果线程池处理微小的快速作业,它的效果最好。

这就是为什么 TPL 允许您指定哪些作业是“长”的。他们希望让您能够缓解线程池的压力。对于基于任务的操作,线程池保持运行非常重要。让它饿死,所有任务都必须等待一些长时间的操作才能完成。这完全不是它的全部内容!

我确信 TPL 会处理该单独线程的创建和管理,专门用于 LongRunning 作业。

至于第二个问题——其实我不知道。 “选择最有效的”通常是一项艰巨的任务,所以我可以肯定地说“不,它并非在所有情况下都这样做”:))) 我认为 TPL 中 DoP/DoC 的扩展是就像将线程池大小调整为机器上逻辑处理器的数量一样简单。 LongRunning 作业的单独线程仍将创建超过限制,因此 ThreadPool 是安全的。将它们包含在 DoP/DoC 限制中会以同样的方式使池饥饿,因为它会减少可用线程的数量。我不认为 TPL 在扩展方面做得更多。也许它会安排在相同线程上对相同数据进行操作的子任务,以获得一些缓存或 NUMA 提升。但我不知道,反正这是一个相当遥远的猜测。

我刚刚发现一篇您可能会感兴趣的文章: New and Improved CLR 4 Thread Pool Engine - 我很确定默认的 TaskScheduler 使用该池。 (来自 JohnSkeet:https://stackoverflow.com/a/4534902/717732

另一个例子,有人对线程池大小和 LongRunning 标志进行了一些测试:Threadpool thread starvation - a practical example

【讨论】:

    【解决方案3】:

    那么在这样的场景下,TPL 本身会不会像上面提到的那样处理线程创建和同步?

    LongRunning Task 只不过是包裹在任务中的线程。这样做的好处是您可以查询其状态、设置延续、等待它并传播错误。您还可以使用组合符,例如 WaitAll

    这就是LongRunning 选项的全部内容。

    它还会自动/内部动态调整并发程度,以最有效地使用所有处理器内核或开发人员需要编写代码来处理所有这些吗?

    您将如何“扩展”单个线程/任务?它本质上是不可扩展的。您需要多个独立的工作单元(例如任务或数据项)来使用多个处理器。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-03-19
      • 2019-07-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多