【问题标题】:What determines the number of threads for a TaskFactory spawned jobs?什么决定了 TaskFactory 生成的作业的线程数?
【发布时间】:2017-02-13 21:48:52
【问题描述】:

我有以下代码:

var factory = new TaskFactory();
for (int i = 0; i < 100; i++)
{
    var i1 = i;
    factory.StartNew(() => foo(i1));
}

static void foo(int i)
{
    Thread.Sleep(1000);
    Console.WriteLine($"foo{i} - on thread {Thread.CurrentThread.ManagedThreadId}");
}            

我可以看到它一次只执行 4 个线程(基于观察)。我的问题:

  1. 是什么决定了一次使用的线程数?
  2. 如何找回这个号码?
  3. 如何更改此号码?

附:我的盒子有 4 个核心。

P.P.S.我需要由 TPL 并发处理的特定数量的任务(而不是更多),并最终得到以下代码:

private static int count = 0;   // keep track of how many concurrent tasks are running

private static void SemaphoreImplementation()
{
    var s = new Semaphore(20, 20);  // allow 20 tasks at a time

    for (int i = 0; i < 1000; i++)
    {
        var i1 = i;

        Task.Factory.StartNew(() =>
        {
            try
            {                        
                s.WaitOne();
                Interlocked.Increment(ref count);

                foo(i1);
            }
            finally
            {
                s.Release();
                Interlocked.Decrement(ref count);
            }
        }, TaskCreationOptions.LongRunning);
    }
}

static void foo(int i)
{
    Thread.Sleep(100);
    Console.WriteLine($"foo{i:00} - on thread " + 
            $"{Thread.CurrentThread.ManagedThreadId:00}. Executing concurently: {count}");
}

【问题讨论】:

  • CPU核心数。
  • @Enigmativity 内核数量对 IO 绑定的工作负载没有意义,TPL 对 IO 毫无头绪。
  • @Enigmativity 所以 TPL 的默认规则是让最大线程数等于 CPU 数?
  • @AngryHacker - 是的,这是我的理解。
  • 为什么不把Semaphore改成SemaphoreSlim

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


【解决方案1】:

当您在 .NET 中使用 Task 时,您是在告诉 TPL 安排一项工作(通过 TaskScheduler)在 ThreadPool 上执行。请注意,工作将尽早安排,但调度员认为合适。这意味着TaskScheduler 将决定使用多少线程来运行n 的任务数量以及哪个任务在哪个线程上执行。

TPL 进行了很好的调整,并在执行您的任务时继续调整其算法。因此,在大多数情况下,它会尽量减少争用。这意味着如果您正在运行 100 个任务并且只有 4 个内核(您可以使用 Environment.ProcessorCount 获得),那么在任何给定时间执行超过 4 个线程是没有意义的,否则它需要做更多上下文切换。现在有时您想明确地覆盖此行为。假设您需要等待某种 IO 完成,这是一个完全不同的故事

总之,相信 TPL。但是,如果您坚持为每个任务生成一个线程(这并不总是一个好主意!),您可以使用:

Task.Factory.StartNew(
    () => /* your piece of work */, 
    TaskCreationOptions.LongRunning);

这告诉 DefaultTaskscheduler 显式地为该工作生成一个新线程。

您也可以使用自己的Scheduler 并将其传递给TaskFactory。你可以找到一大堆SchedulersHERE

请注意,另一种选择是使用 PLINQ 再次默认情况下分析您的查询并决定并行化它是否会产生任何好处,同样在您所在的阻塞 IO 的情况下某些启动多个线程将导致更好的执行,您可以使用 WithExecutionMode(ParallelExecutionMode.ForceParallelism) 强制并行,然后可以使用 WithDegreeOfParallelism,提示使用多少线程 但是请记住,正如MSDN 所说,不保证你会得到那么多线程:

设置要在查询中使用的并行度。程度 并行度是并发执行任务的最大数量 将用于处理查询。

最后,我强烈推荐阅读THISThreadingTPL 上的一系列精彩文章。

【讨论】:

  • TPL 不知道工作负载的作用。它不可能针对 IO 进行了很好的调整。 TPL“会做正确的事”的想法在整个网络上重复,但都是错误的。这个问题就是一个很好的例子:4 个线程是吞吐量限制的选择。
  • 因此我说:“一个完全不同的故事”!
  • @usr 正是我的想法。 TPL 对我的工作流程一无所知。我最终用TaskCreationOptions.LongRunning 加上一个信号量来实现它。
【解决方案2】:

如果您将任务数增加到例如 1000000,您将看到随着时间的推移产生更多的线程。 TPL 倾向于每 500 毫秒注入一次。

TPL 线程池不理解受 IO 限制的工作负载(睡眠就是 IO)。在这些情况下,依靠 TPL 来选择正确的并行度不是一个好主意。 TPL 完全没有头绪,并且基于对吞吐量的模糊猜测注入了更多线程。也是为了避免死锁。

在这里,TPL 策略显然没有用,因为添加的线程越多,获得的吞吐量就越大。在这种人为的情况下,每个线程每秒可以处理一个项目。 TPL 对此一无所知。将线程数限制为内核数是没有意义的。

什么决定了一次使用的线程数?

几乎没有记录的 TPL 启发式方法。他们经常出错。特别是在这种情况下,它们会随着时间的推移产生无限数量的线程。使用任务管理器自己查看。让它运行一个小时,您将拥有 1000 多个线程。

我怎样才能找回这个号码?如何更改此号码?

您可以检索一些这些数字,但这不是正确的方法。如果您需要有保证的 DOP,您可以使用 AsParallel().WithDegreeOfParallelism(...) 或自定义任务调度程序。您也可以手动启动LongRunning 任务。不要乱用进程全局设置。

【讨论】:

  • DOP 是什么意思?使用AsParallel() 在阻塞 IO 的情况下,您仍然需要通过使用 WithDegreeOfParallelism 给它提示,否则它认为工作受 CPU 限制并以不同方式分配线程,因此再次在 4 核机器上 PLINQ 可能只运行 4同时执行任务。
  • @MaYaN 是的,无论你怎么做,你总是需要指定一个涉及 IO 的 DegreeOfParallelism (=DOP)。这是一个关键点:没有库可以知道正确的值。您需要凭经验(或通过人类猜测)找到它。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-12-24
  • 1970-01-01
  • 2023-04-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多