客户端的.NET并发性能

时间:2010-03-22 19:20:24

标签: .net performance multithreading concurrency scalability

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

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

5 个答案:

答案 0 :(得分:12)

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

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

  • 串行执行(无线程)
  • 使用ThreadPool
  • 4个主题(即每个主目录一个)
  • 使用ThreadPool的40个线程(以测试池本身的效率)
  • 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

结论可以从中得出结论:

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

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

  • 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);
            }
        }
    }
}

这是主要的发电机:

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]);
        }
    }
}

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

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

答案 1 :(得分:9)

您可能希望了解.NET 4中引入的System.Threading.Tasks

他们引入了一种可扩展的方式,将线程与任务结合使用,并采用了一些非常酷的工作共享机制。

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

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

答案 2 :(得分:7)

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

如果可以的话,我建议使用.NET 4和任务并行库。它简化了许多并发问题。有关详细信息,我建议您查看Parallel Computing with Managed Code的MSDN中心。

如果您对实施细节感兴趣,我还会对Parallelism in .NET进行非常详细的介绍。

答案 3 :(得分:4)

并发性上的.NET性能与使用本机代码编写的应用程序非常接近。 System.Threading是线程API上的一个非常薄的层。

警告你的人可能会注意到,因为多线程应用程序在.NET中更容易编写,它们有时是由经验不足的程序员编写的,他们并不完全理解并发性,但这不是技术限制。

如果轶事证据有所帮助,在我上一份工作中,我们编写了一个大量并发的交易应用程序,每秒处理超过20,000个市场数据事件,并通过相当大规模的线程架构更新了包含相关数据的大型“主窗体”网格以及所有在C#和VB.NET中。由于应用程序的复杂性,我们优化了许多领域,但从未看到在本机C ++中重写线程代码的优势。

答案 4 :(得分:3)

首先,您应该认真考虑是否需要大量线程或仅需要一些线程。并不是.NET线程很慢。线程很慢。无论谁编写算法,任务切换都是一项昂贵的操作。

这个地方和许多其他地方一样,设计模式可以提供帮助。已经有很好的答案触及了这个事实,所以我只是说明一点。你最好使用命令模式将工作编组到一些工作线程中,然后按顺序尽可能快地完成工作,而不是尝试启动一堆线程并在“并行”中执行大量工作并不是真正并行完成,而是分成由调度程序编织在一起的小块。

换句话说:你最好用你的思想和知识将工作划分为多块价值,以决定价值单元之间的界限在哪里,而不是像操作系统那样的通用解决方案为你决定。