我正在编写一个客户端.NET应用程序,预计会使用很多线程。我被警告说,在并发性方面.NET性能非常糟糕。虽然我不是在编写实时应用程序,但我想确保我的应用程序是可伸缩的(即允许多个线程)并且某种程度上与同等的C ++应用程序相当。
你有什么经历?什么是相关基准?
答案 0 :(得分:12)
我使用素数生成器作为测试,在C#中汇总了一个快速而肮脏的基准。该测试使用简单的Eratosthenes Sieve实现生成质数达到常数限制(我选择500000)并重复测试800次,并使用.NET ThreadPool
或独立线程在特定数量的线程上并行化。
测试在运行Windows Vista(x64)的四核Q6600上运行。这不是使用任务并行库,只是简单的线程。它是针对以下场景运行的:
ThreadPool
ThreadPool
的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线程很慢。线程很慢。无论谁编写算法,任务切换都是一项昂贵的操作。
这个地方和许多其他地方一样,设计模式可以提供帮助。已经有很好的答案触及了这个事实,所以我只是说明一点。你最好使用命令模式将工作编组到一些工作线程中,然后按顺序尽可能快地完成工作,而不是尝试启动一堆线程并在“并行”中执行大量工作并不是真正并行完成,而是分成由调度程序编织在一起的小块。
换句话说:你最好用你的思想和知识将工作划分为多块价值,以决定价值单元之间的界限在哪里,而不是像操作系统那样的通用解决方案为你决定。