客户端的.NET并发性能

Yar*_*veh 4 .net concurrency performance multithreading scalability

我正在编写一个客户端.NET应用程序,预计会使用很多线程.我被警告说,在并发性方面.NET性能非常糟糕.虽然我不是在编写实时应用程序,但我想确保我的应用程序是可伸缩的(即允许多个线程),并且在某种程度上可以与同等的C++应用程序相媲美.

你有什么经历?什么是相关基准?

Aar*_*ght 12

我使用一个素数发生器作为测试,在C#中汇总了一个快速而肮脏的基准.该测试使用简单的Eratosthenes Sieve实现生成质数达到常数限制(我选择500000)并重复测试800次,并使用.NET ThreadPool或独立线程在特定数量的线程上并行化.

测试在运行Windows Vista(x64)的四核Q6600上运行.这不是使用任务并行库,只是简单的线程.它针对以下场景运行:

  • 串行执行(无线程)
  • 4个线程(即每个核心一个),使用 ThreadPool
  • 40个线程使用ThreadPool(测试池本身的效率)
  • 4个独立线程
  • 40个独立线程,用于模拟上下文切换压力

结果是:

Test | Threads | ThreadPool | Time
-----+---------+------------+--------
1    | 1       | False      | 00:00:17.9508817
2    | 4       | True       | 00:00:05.1382026
3    | 40      | True       | 00:00:05.3699521
4    | 4       | False      | 00:00:05.2591492
5    | 40      | False      | 00:00:05.0976274
Run Code Online (Sandbox Code Playgroud)

结论可以从中得出:

  • 并行化并不完美(正如预期的那样 - 无论环境如何都是如此),但是将负载分成4个核心会导致吞吐量增加3.5倍,这几乎不值得抱怨.

  • 使用4到40个线程之间的差异可以忽略不计ThreadPool,这意味着即使你用请求轰炸它,也不会对池产生大量费用.

  • ThreadPool和自由线程版本之间的差异可以忽略不计,这意味着ThreadPool它没有任何重大的"常数"费用;

  • 4线程和40线程自由线程版本之间的差异可以忽略不计,这意味着.NET的执行速度不会超过人们对大量上下文切换的预期.

我们甚至需要一个C++基准来比较吗?结果非常清楚:.NET中的线程并不慢.除非,程序员,编写糟糕的多线程代码并最终导致资源匮乏或锁定车队,否则你真的不必担心.

使用.NET 4.0和TPL以及对ThreadPool工作窃取队列和所有那些很酷的东西的改进,你有更多的余地来编写"有问题"的代码并且仍然可以高效运行.你根本没有从C++中获得这些功能.

供参考,这是测试代码:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading;

namespace ThreadingTest
{
    class Program
    {
        private static int PrimeMax = 500000;
        private static int TestRunCount = 800;

        static void Main(string[] args)
        {
            Console.WriteLine("Test | Threads | ThreadPool | Time");
            Console.WriteLine("-----+---------+------------+--------");
            RunTest(1, 1, false);
            RunTest(2, 4, true);
            RunTest(3, 40, true);
            RunTest(4, 4, false);
            RunTest(5, 40, false);
            Console.WriteLine("Done!");
            Console.ReadLine();
        }

        static void RunTest(int sequence, int threadCount, bool useThreadPool)
        {
            TimeSpan duration = Time(() => GeneratePrimes(threadCount, useThreadPool));
            Console.WriteLine("{0} | {1} | {2} | {3}",
                sequence.ToString().PadRight(4),
                threadCount.ToString().PadRight(7),
                useThreadPool.ToString().PadRight(10),
                duration);
        }

        static TimeSpan Time(Action action)
        {
            Stopwatch sw = new Stopwatch();
            sw.Start();
            action();
            sw.Stop();
            return sw.Elapsed;
        }

        static void GeneratePrimes(int threadCount, bool useThreadPool)
        {
            if (threadCount == 1)
            {
                TestPrimes(TestRunCount);
                return;
            }

            int testsPerThread = TestRunCount / threadCount;
            int remaining = threadCount;
            using (ManualResetEvent finishedEvent = new ManualResetEvent(false))
            {
                for (int i = 0; i < threadCount; i++)
                {
                    Action testAction = () =>
                    {
                        TestPrimes(testsPerThread);
                        if (Interlocked.Decrement(ref remaining) == 0)
                        {
                            finishedEvent.Set();
                        }
                    };

                    if (useThreadPool)
                    {
                        ThreadPool.QueueUserWorkItem(s => testAction());
                    }
                    else
                    {
                        ThreadStart ts = new ThreadStart(testAction);
                        Thread th = new Thread(ts);
                        th.Start();
                    }
                }
                finishedEvent.WaitOne();
            }
        }

        [MethodImpl(MethodImplOptions.NoOptimization)]
        static void IteratePrimes(IEnumerable<int> primes)
        {
            int count = 0;
            foreach (int prime in primes) { count++; }
        }

        static void TestPrimes(int testRuns)
        {
            for (int t = 0; t < testRuns; t++)
            {
                var primes = Primes.GenerateUpTo(PrimeMax);
                IteratePrimes(primes);
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这里是素数发生器:

using System;
using System.Collections.Generic;
using System.Linq;

namespace ThreadingTest
{
    public class Primes
    {
        public static IEnumerable<int> GenerateUpTo(int maxValue)
        {
            if (maxValue < 2)
                return Enumerable.Empty<int>();

            bool[] primes = new bool[maxValue + 1];
            for (int i = 2; i <= maxValue; i++)
                primes[i] = true;

            for (int i = 2; i < Math.Sqrt(maxValue + 1) + 1; i++)
            {
                if (primes[i])
                {
                    for (int j = i * i; j <= maxValue; j += i)
                        primes[j] = false;
                }
            }

            return Enumerable.Range(2, maxValue - 1).Where(i => primes[i]);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

如果您在测试中发现任何明显的缺陷,请告诉我.除非测试本身出现任何严重问题,我认为结果不言自明,而且信息很明确:

不要聆听那些对.NET或任何其他语言/环境的性能如何在某个特定领域"糟糕"做出过于宽泛和无条件声明的人,因为他们可能正在谈论他们的......后端.


小智 9

您可能希望了解System.Threading.Tasks.NET 4 中介绍的内容.

他们介绍了一种可扩展的方式,将线程与任务结合使用,并采用了一些非常酷的作

顺便说一下,我不知道是谁告诉你.NET并不适合并发.我的所有应用程序确实在另一个应用程序的某个位置使用线程,但不要忘记在2核处理器上有10个线程会产生相反的效果(取决于你正在进行的任务的类型.如果它的任务是等待网络资源然后它可能有意义).

无论如何,不​​要害怕.NET的性能,它实际上非常好.


Ree*_*sey 7

这是一个神话..NET在管理并发性方面做得非常好,并且具有很高的可扩展性.

如果可以,我建议使用.NET 4和任务并行库.它简化了许多并发问题.有关详细信息,我建议您查看带有托管代码的并行计算的MSDN中心.

如果您对实现的细节感兴趣,我还有一个关于.NET中Parallelism的非常详细的系列文章.