代码之家  ›  专栏  ›  技术社区  ›  Yaron Naveh

客户端的.NET并发性能

  •  4
  • Yaron Naveh  · 技术社区  · 15 年前

    我正在编写一个客户端.NET应用程序,它需要使用大量线程。有人警告我,在并发性方面.NET性能非常差。虽然我不是在编写实时应用程序,但我想确保我的应用程序是可伸缩的(即允许多线程),并且 以某种方式 相当于一个等价的C++应用程序。

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

    5 回复  |  直到 15 年前
        1
  •  12
  •   Aaronaught    15 年前

    我在C中构建了一个快速而肮脏的基准,使用一个主生成器作为测试。该测试使用一个简单的Eratostenes实现筛选,生成一个恒定极限的素数(我选择了500000个素数),并重复测试800次,在特定数量的线程上并行,或者使用.NET ThreadPool 或独立线程。

    测试是在运行WindowsVista(x64)的四核Q6600上运行的。这不是使用任务并行库,而是简单的线程。它在以下情况下运行:

    • 串行执行(无线程)
    • 4个螺纹(即每个芯一个),使用 螺纹工具
    • 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倍的吞吐量,这几乎没有什么可抱怨的。

    • 使用 线程池 这意味着,即使您用请求轰炸池,也不会对池产生重大的开销。

    • 两者之间的差异可以忽略不计。 线程池 和自由线程版本,这意味着 线程池 没有任何重大的“固定”费用;

    • 4线程和40线程的无线程版本之间的差异可以忽略不计,这意味着.NET的性能不会比人们在繁重的上下文切换中预期的差。

    我们甚至需要一个C++基准来比较吗?结果很明显,.NET中的线程并不慢。除非 对于程序员来说,编写糟糕的多线程代码并最终导致资源不足或锁保护,您真的不需要担心。

    与.NET 4.0和TPL以及对 线程池 在窃取队列和其他一些很酷的东西的时候,您甚至有更多的空间来编写“有问题”的代码,并且仍然有效地运行它。你不能从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或任何其他语言/环境在某些特定领域的性能如何“糟糕”做出过于宽泛和无保留声明的人说的话,因为他们可能在谈论他们的…后端。

        2
  •  9
  •   Peter Mortensen icecrime    15 年前

    你可能想看看 System.Threading.Tasks 在.NET 4中引入。

    他们引入了一种可扩展的方法,通过一些非常酷的工作共享机制来使用带有任务的线程。

    顺便说一下,我不知道谁告诉过你.NET不适合并发性。我的所有应用程序都会在另一个地方使用线程,但不要忘记在2核处理器上拥有10个线程有点适得其反(取决于您让它们执行的任务类型)。如果是等待网络资源的任务,那么它可能是有意义的)。

    不管怎样,不要担心.NET的性能,它实际上相当好。

        3
  •  7
  •   Reed Copsey    15 年前

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

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

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

        4
  •  4
  •   Peter Mortensen icecrime    15 年前

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

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

    如果奇闻轶事的证据有帮助,在我上次的工作中,我们编写了一个高度并发的交易应用程序,它每秒处理超过20000个市场数据事件,并用相关数据更新了一个巨大的“主窗体”网格,所有这些都是通过一个相当庞大的线程体系结构完成的,都是在C和VB.NET中完成的。由于应用程序的复杂性,我们对多个领域进行了优化,但从未发现在原生C++中改写线程代码的优势。

        5
  •  3
  •   MaxGuernseyIII    15 年前

    首先,您应该认真地重新考虑您是否需要大量的线程或只是一些线程。不是.NET线程很慢。线程速度慢。不管是谁编写的算法,任务切换都是一项昂贵的操作。

    这是一个地方,像许多其他地方一样,设计模式可以帮助。关于这个事实,已经有了很好的答案,所以我会把它说清楚。您最好使用命令模式将工作封送到几个工作线程中,然后按顺序尽可能快地完成这些工作,而不是试图旋转一组线程并以“并行”方式执行一组实际上不是并行完成的工作,而是将这些工作分成由调度器。

    换言之:你最好用你的头脑和知识来决定价值单位之间的界限,而不是让一些通用的解决方案(如操作系统)为你决定。