为什么具有2个线程的8个进程每个创建的负载比具有16个线程的进程多?

时间:2012-02-17 14:53:06

标签: c# multithreading

我有一个简单的程序,它启动n个线程并在每个线程上创建一些负载。 如果我只启动一个线程,则一个核心获得大约100%的负载。 如果我用16个线程(这意味着每个核心一个线程)启动一个进程,我只能获得大约80%的负载。 如果我用2个线程启动8个进程(这仍然意味着每个核心一个线程),我得到大约99%的负载。 我在这个样本中没有使用任何锁定。

这种行为的原因是什么? 据我所知,如果有100个线程正在工作,那么负载会下降,因为操作系统必须安排很多。 但在这种情况下,只有与核心一样多的线程。

情况更糟(至少对我而言)。 如果我在循环中添加一个简单的thread.sleep(0),那么一个进程和16个线程的负载增加到95%。

任何人都可以回答这个问题,或提供有关此特定主题的更多信息的链接吗?

One Process 16 threads

Eight Process 2 threads

One Process 16 threads with thread.sleep(0)

//Sample application which reads the number of threads to be started from Console.ReadLine
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Enter the number of threads to be started");
            int numberOfThreadsToStart;

            string input = Console.ReadLine();

            int.TryParse(input, out numberOfThreadsToStart);
            if(numberOfThreadsToStart < 1)
            {
                Console.WriteLine("No valid number of threads entered. Exit now");
                Thread.Sleep(1500);
                return;
            }

            List<Thread> threadList = new List<Thread>();
            Stopwatch sw = Stopwatch.StartNew();
            for (int i = 0; i < numberOfThreadsToStart; i++)
            {
                Thread workerThread = new Thread(MakeSomeLoad);
                workerThread.Start();
                threadList.Add(workerThread);
            }

            while (true)
            {
                Console.WriteLine("I'm spinning... ");
                Thread.Sleep(2000);
            }
        }

        static void MakeSomeLoad()
        {
            for (int i = 0; i < 100000000; i++)
            {

                for (int j = 0; j < i; j++)
                {
                    //uncomment the following line to increase the load
                    //Thread.Sleep(0);
                    StringBuilder sb = new StringBuilder();
                    sb.Append("hello world" + j);
                }
            }
        }
    }

4 个答案:

答案 0 :(得分:6)

你的测试看起来很重。如果在一个进程中有16个线程,则GC将在该进程中运行更多,并且由于客户端GC不是并行的,因此会导致负载降低。即每个GC线程有16个垃圾生成线程。

另一方面,如果你运行8个进程,每个进程有两个线程,你只得到两个线程为每个GC线程产生垃圾,GC可以在这些进程之间并行工作。

如果您编写的测试产生的垃圾较少,并且直接使用更多CPU,则可能会得到不同的结果。

(请注意,这只是推测,我没有运行您的测试,因为我只有一个与您的结果不同的双核CPU)

答案 1 :(得分:4)

要考虑的其他事情是垃圾收集器有不同的模式:

  • 服务器GC
  • Workstation GC - 并发(asp.net的默认值)
  • 工作站GC - 非并发

您可以找到每个here的一些图形细节。

由于您的进程使用大量线程并分配了大量内存,因此您应该尝试使用服务器GC。

  

服务器GC针对高吞吐量和高可扩展性进行了优化   服务器应用程序,其中存在一致的负载和请求   以高速率分配和释放内存。服务器GC使用   每个处理器一个堆和一个GC线程,并尝试平衡   尽可能地堆积。在垃圾收集时,GC   线程在它们各自的线程上工作,并且肯定会更新   点。因为它们都在自己的堆上工作,最小的锁定等。   需要这使得它在这种情况下非常有效。

在App.config中启用Server CG:

<configuration>
 <runtime>
   <gcServer enabled="true" />
 </runtime>
</configuration> 

请注意,这仅适用于多处理器(或核心)系统。如果Windows只报告一个处理器,那么您将获得Workstation GC - 非并发。

答案 2 :(得分:1)

使用像Thread.SpinWait(int.MaxValue)之类的东西来产生CPU负载,因为你的程序主要产生内存负载,这可能会导致错误共享等效果。正如CodeInChaos已经指出的那样,GC活动也很可能会影响性能。

答案 3 :(得分:1)

与其他人一样,我怀疑这与GC有关。加载示例使用巨大内存量,在两个for循环结束时,StringBuilder对象将要求使用千兆字节大小的数组来存储其数据。

GC线程可能会降低处理速度,这有几个原因。

一个是,一旦VM耗尽内存,大多数线程将不得不暂停并等待GC释放内存,然后才能继续(这是因为所有线程都会要求更多内存)执行期间大致相同的时间。)

第二个是线程的上下文切换(这可能是最大的原因)。如果在核心X上运行的线程A用完了内存,则GC必须被加载到核心X或者将核心X的高速缓存中的所有线程A的内存加载到它正在运行的核心上的高速缓存中。无论哪种方式,CPU都必须等待其缓存从RAM加载内存。 RAM与硬盘驱动器相比速度很快,但与CPU相比,它的速度非常慢。当CPU等待RAM响应时,它无法进行任何处理,从而减少了负载。

当您有多个虚拟机时,每个虚拟机都可以在自己的核心上运行,而不关心其他虚拟机的用途。当调用GC时,不需要上下文切换,因为GC可以在与VM上其他两个线程相同的内核上运行。