【问题标题】:Multiple Tasks slows down多任务减慢
【发布时间】:2011-09-13 05:13:07
【问题描述】:

代码:

static void DoIt(string name)
{
    Console.WriteLine("Hello {0} | {1}", name, Thread.CurrentThread.ManagedThreadID);
    Thread.Sleep(5000);
    Console.WriteLine("Bye {0} | {1}", name, Thread.CurrentThread.ManagedThreadID);
}

static void Main()
{
    Task.Factory.StartNew(() => DoIt("One"));
    Task.Factory.StartNew(() => DoIt("Two"));
    Task.Factory.StartNew(() => DoIt("Three"));
    Task.Factory.StartNew(() => DoIt("Four"));
    Task.Factory.StartNew(() => DoIt("Five"));
    Task.Factory.StartNew(() => DoIt("Six"));
    Task.Factory.StartNew(() => DoIt("Seven"));
    Task.Factory.StartNew(() => DoIt("Eight"));
    Task.Factory.StartNew(() => DoIt("Nine"));
    Task.Factory.StartNew(() => DoIt("Ten"));

    Console.ReadKey();
}

为什么它可以立即启动前 3 个任务,但是任务 4 启动需要 5-10 秒,任务 4 启动后,任务 5 启动需要 5-10 秒等等。是 GC 在做某事吗?有人可以澄清发生了什么吗?

【问题讨论】:

  • 您认为您的 CPU 有多少个内核?在这种情况下,这不是一个细节...... :)
  • 我在一个启动 100 个任务的网络 IO 密集型应用程序中看到了相同的行为。实际上需要 100 个任务才能接近最大系统吞吐量,但后面的任务需要很长时间才能启动。
  • @Davide:你知道 TPL 假设任务是 CPU 绑定的还是只是假设?很高兴知道情况是否如此。
  • 我什么都不知道...我猜。 :) 在我的猜测中,创建多个执行单元(核心)数量较少的并发操作只能意味着整体执行速度比一个接一个地做事要慢,如果没有其他事情的话,至少会有一些上下文切换或调度的开销。反正这里不是 TPL 专家。
  • 你有 AMD 3 核的机器吗?

标签: c# multithreading


【解决方案1】:

为什么它可以立即启动前 3 个任务,但是任务 4 启动需要 5-10 秒,任务 4 启动后,任务 5 启动需要 5-10 秒等等。是 GC 在做某事吗?有人可以澄清发生了什么吗?

默认情况下,第一次运行时,ThreadPool 是使用最少数量的工作线程分配的。在安排好前 4 个任务后,线程池将随着时间的推移“加速”处理更多任务,这就是您看到延迟的原因。

在我的系统(有 8 个核心)上,前 8 个是瞬时的,接下来的两个会在一秒钟后启动。

在您的情况下,如果您运行两次测试,即 第二次 次,所有线程都会立即启动。这是因为,在第一次运行之后,线程池应该有足够的工作人员来立即安排这个。

尝试以下操作以查看此行为的实际效果。如果您保留 SetMinThreads 调用,这些将立即安排。如果你把它注释掉,你会看到,第一次,它需要一段时间,但第二次通过(假设你等待线程完成),线程将立即运行。

static void DoIt(string name)
{
    Console.WriteLine("Hello {0} | {1} - {2}", name, Thread.CurrentThread.ManagedThreadId, DateTime.Now);
    Thread.Sleep(5000);
    Console.WriteLine("Bye {0} | {1} - {2}", name, Thread.CurrentThread.ManagedThreadId, DateTime.Now);
}

static void Main()
{
    int workerThreads, complete;
    ThreadPool.GetMinThreads(out workerThreads, out complete);

    Console.WriteLine(workerThreads);

    // Comment out this line to see the difference...
    // WIth this commented out, the second iteration will be immediate
    ThreadPool.SetMinThreads(100, complete);

    Action run = () =>
        {
            for (int i = 0; i < 20; ++i)
            {
                int tmp = i;
                Task.Factory.StartNew(() => DoIt(tmp.ToString()));
            }
        };

    run();
    Console.WriteLine("Press a key to run again...");
    Console.ReadKey();

    run();

    Console.WriteLine("Press a key to exit...");
    Console.ReadKey();
}

请注意,这种行为实际上与整个 TPL 几乎没有关系——它更多的是使用默认的TaskScheduler,它只是将任务传递给ThreadPool。例如,如果您在 StartNew() 调用中使用 LongRunning 提示设置这些线程,它们都会立即启动(因为默认调度程序将设置一个新的专用线程并立即执行它)。

【讨论】:

  • 感谢您提供非常详细的答案! - 如果我注释掉ThreadPool.SetMinThreads(100, complete),并运行两次,那么第二次它不会立即启动所有任务,而只会启动第一个 15-17。其他人怎么了?
  • @ebb:根据您在两次点击之间花费的时间,线程池将开始关闭线程(回到最小值...)您等待的时间越长,立即启动的线程就越少。
【解决方案2】:

任务并没有变慢,它们正在被任务并行库排队。 CLR 知道您的计算机上有多少逻辑内核可用; TPL 线程池使用此信息来确定有多少工作线程可用。在您的情况下,您可能有四个逻辑核心;为主线程(Main() 正在运行)拿走一个,三个核心(和三个线程池工作线程)仍然用于执行任务。

【讨论】:

  • 仅供参考 - 这实际上是在这种情况下做出此决定的 ThreadPool,而不是 TPL...
  • 你确实是对的;我已经编辑了我的答案,使其更加精确。
  • BTW - 我怀疑这是一台 3 核机器 - UI/Main 线程通常不是 ThreadPool 线程,因此 OP 只获得 3 个任务这一事实表明他们可能有类似的东西:@ 987654321@
  • 虽然你可能是对的(我当然不知道我的头脑),但我认为线程池至少占进程中一个先前运行的线程。
  • 打得好,先生。这比我考虑的更有意义。 :)
【解决方案3】:

TPL 包含适应工作负载的非常复杂的算法。对于 4 核机器,您所遇到的情况听起来很合适——TPL 决定做一些它认为在创建如此多线程的情况下有益的事情。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-08-21
    • 2017-06-06
    • 1970-01-01
    • 2016-01-22
    • 2012-11-04
    • 2015-03-15
    • 1970-01-01
    相关资源
    最近更新 更多