【问题标题】:Why Thread.Start method is blocked when CPU load is high?为什么 Thread.Start 方法在 CPU 负载高时被阻塞?
【发布时间】:2015-12-31 22:28:36
【问题描述】:

出于测试目的,我编写了 CPU 压力程序:它只是在 M 个线程中执行 N 个 for 循环。 我用大量线程运行这个程序,比如 200。 但是在任务管理器中,我看到线程计数器没有超过一些小的值,比如 9 和一个 Thread.Start 方法等待完成以前运行的线程

这种行为似乎是 ThreadPool 行为,但我希望常规的 System.Threading.Thread 无论如何都必须启动,而无需等待。

以下代码将重现此问题并提供解决方法:

using System;
using System.Diagnostics;
using System.Threading;

namespace HeavyLoad
{
    class Program
    {
        static long s_loopsPerThread;
        static ManualResetEvent s_startFlag;

        static void Main(string[] args)
        {
            long totalLoops = (long)5e10;
            int threadsCount = 200; 

            s_loopsPerThread = totalLoops / threadsCount;

            Thread[] threads = new Thread[threadsCount];

            var watch = Stopwatch.StartNew();
            for (int i = 0; i < threadsCount; i++)
            {
                Thread t = new Thread(IntensiveWork);
                t.IsBackground = true;
                threads[i] = t;
            }
            watch.Stop();
            Console.WriteLine("Creating took {0} ms", watch.ElapsedMilliseconds);

            // *** Comment out s_startFlag creation to change the behavior ***
            // s_startFlag = new ManualResetEvent(false);

            watch = Stopwatch.StartNew();
            foreach (var thread in threads)
            {
                thread.Start();
            }
            watch.Stop();
            Console.WriteLine("Starting took {0} ms", watch.ElapsedMilliseconds);

            if (s_startFlag != null)
                s_startFlag.Set();

            watch = Stopwatch.StartNew();
            foreach (var thread in threads)
            {
                thread.Join();
            }
            watch.Stop();
            Console.WriteLine("Waiting took {0} ms", watch.ElapsedMilliseconds);

            Console.ReadLine();
        }

        private static void IntensiveWork()
        {
            if (s_startFlag != null)
                s_startFlag.WaitOne();

            for (long i = 0; i < s_loopsPerThread; i++)
            {
                // hot point
            }
        }
    }
}

案例 1: 如果注释了 s_startFlag 创建,则启动线程立即开始高强度 CPU 工作。在这种情况下,我的并发性很小(大约 9 个线程),并且一直保持线程启动代码:

Creating took 0 ms
Starting took 4891 ms
Waiting took 63 ms

案例 2: 但是如果我创建 s_startFlag,所有新线程都会等到它被设置。在这种情况下,我成功地同时启动了所有 200 个线程并获得了预期值:启动时间短,工作时间长,任务管理器中的线程数为 200+:

Creating took 0 ms
Starting took 27 ms
Waiting took 4733 ms

为什么线程在第一种情况下拒绝启动?我超出了什么样的限制?

系统:

  • 操作系统:Windows 7 专业版
  • 框架:NET 4.6
  • CPU:Intel Core2 Quad Q9550 @ 2.83GHz
  • 内存:8 Gb

【问题讨论】:

  • 您可能正在查看硬件/操作系统限制?例如,磁盘 I/O 肯定会有最大通道限制,这实际上会导致某些线程被阻塞......
  • 此测试不涉及任何磁盘 I/O。这似乎是限制“你有 95% 的 CPU 负载,所以我不启动任何线程”。但我没有听说过这样的限制。为什么会这样?
  • 问题是,究竟是什么导致了 CPU 负载? CPU 正在做某事,这就是仪器告诉你的。此时运行分析器将非常有意义。
  • 没有。我知道为什么 CPU 负载很高——我特意做的(长循环)。我的问题是为什么线程不能快速启动。
  • 我的猜测也是,因为线程刚刚开始占用处理能力,您自己的线程会大大减慢速度。也许尝试线程优先级,然后看看会发生什么。此外,您的 CPU 有 4 个内核,我真的不相信当 cputime 均等共享时,每个内核都可以处理两个以上的线程。计数器也显示 9... 在我看来这是 1 个主线程 + 4 * 2 个线程。

标签: c# multithreading benchmarking


【解决方案1】:

我做了一些研究,现在我发现高 CPU 负载确实对线程启动时间有很大影响。

首先:我将 totalLoops 设置为大 100 倍的值,以便有更多的观察时间。我看到线程不受限制,但创建速度非常缓慢。 1-2 秒后启动 1 个线程!

第二:我使用 SetThreadAffinityMask 函数 (https://sites.google.com/site/dotburger/threading/setthreadaffinitymask-1) 将主线程显式绑定到 CPU 内核 #0,并将工作线程绑定到内核 #1、#2、#3。

Stopwatch watch;
using (ProcessorAffinity.BeginAffinity(0))
{
    watch = Stopwatch.StartNew();
    for (int i = 0; i < threadsCount; i++)
    {
        Thread t = new Thread(IntensiveWork);
        t.IsBackground = true;
        threads[i] = t;
    }
    watch.Stop();
    Console.WriteLine("Creating took {0} ms", watch.ElapsedMilliseconds);
}

using (ProcessorAffinity.BeginAffinity(1, 2, 3))
{
    for (long i = 0; i < s_loopsPerThread; i++)
    {
    }
}

现在主线程有自己的专用 CPU 核心(在进程边界中)和工作线程在大约 10 毫秒后开始(totalLoops = 5e10)。

Creating took 0 ms
Starting took 2282 ms
Waiting took 3681 ms

另外,我在MSDN中找到了这句话:

当您在线程上调用 Thread.Start 方法时,该线程可能 或可能不会立即开始执行,具体取决于 处理器和当前等待执行的线程数。

https://msdn.microsoft.com/en-us/library/1c9txz50(v=vs.110).aspx

结论: Thread.Start 方法对活跃工作线程的数量非常敏感。这可能会对性能产生非常强烈的影响 - 会减慢数百倍。

【讨论】:

    猜你喜欢
    • 2020-08-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-03-14
    • 1970-01-01
    • 2017-01-26
    相关资源
    最近更新 更多