在现代软件开发中,多线程应用的设计和优化是一个重要的议题。本文将通过一个具体案例,探讨在C++和.NET环境下,多线程应用中内存分配对性能的影响,并提供相应的优化策略。
在C++中,标准模板库(STL)的使用极大地简化了数据结构的操作,但这也带来了内存分配的问题。在多线程环境中,STL的内存分配机制可能会导致性能瓶颈。
例如,一个依赖于STL的多线程应用可能会进行大量的内存分配和释放操作。尽管这些操作可以被优化,但STL隐藏了内存分配的细节,使得替换分配器变得不切实际。
在多线程版本中,性能反而比单线程应用更慢,这在一开始是令人困惑的。理论上,任务可以被均匀分配到可用的核心上,且工作之间没有通信,与主线程的同步也仅仅是“这里有工作要做”,而实际工作占据了99%的处理时间,远超过队列锁定机制。
深入研究后发现,C++中的内存分配虽然是线程安全的,但实际上是一个单线程函数。当一个线程请求分配或释放内存时,所有其他线程都会被阻塞。这种情况是不可接受的,因此解决方案是启动单独的物理进程,每个CPU一个,并通过管道与主应用线程通信。由于通信开销很低,这不是问题。最终的结果是每个核心都被100%利用,整体处理时间也随着核心数的增加而线性减少。
.NET环境下的内存分配行为与C++有所不同。首先,通过一个简单的测试案例来确保一切正常工作。测试案例不进行内存分配,而是重复计算100的阶乘。
static int FACTORIAL_OF = 100;
static void ThreadTest()
{
List<Thread> threads = new List<Thread>();
List<Worker> workers = new List<Worker>();
int n = Environment.ProcessorCount;
for (int i = 0; i < n; i++)
{
Worker worker = new Worker(FactorialTest);
Thread thread = new Thread(worker.DoWork);
workers.Add(worker);
threads.Add(thread);
}
threads.ForEach(t => t.Start());
Console.WriteLine("\nPress ENTER key to stop...");
Console.ReadLine();
workers.ForEach(w => w.RequestStop());
threads.ForEach(t => t.Join());
Console.WriteLine("\nDone");
}
在8核心系统上,所有处理器都达到了100%的利用率。
现在,让尝试同样的测试,但是这次是在线程中分配10,000个16K大小的内存块,然后立即丢弃,以便下一次分配10,000个对象。
Worker worker = new Worker(AllocationTest);
static int ALLOCATIONS = 10000;
static int ALLOCATION_SIZE = 16384;
static void AllocationTest()
{
Console.WriteLine(AppDomain.CurrentDomain.FriendlyName);
object[] objects = new object[ALLOCATIONS];
for (int i = 0; i < ALLOCATIONS; i++)
{
objects[i] = new byte[ALLOCATION_SIZE];
}
}
结果令人惊讶,CPU利用率仅为33%,而且只有四个核心在工作。这表明,与C++一样,.NET中的内存管理在分配内存时也会阻塞。
让尝试将测试作为独立进程运行。
static void ProcessTest()
{
List<Process> processes = new List<Process>();
int n = Environment.ProcessorCount;
for (int i = 0; i < n; i++)
{
Process p = Process.Start("ProcessWorker.exe");
processes.Add(p);
}
Console.WriteLine("\nPress ENTER key to stop...");
Console.ReadLine();
processes.ForEach(p => p.Kill());
Console.WriteLine("\nDone");
}
结果表明,由于每个测试都在自己的进程中运行,内存分配不会跨进程阻塞。
在.NET中,通过在app.config文件中设置,可以启用服务器模式的垃圾回收。
<runtime>
<gcServer enabled="true"/>
</runtime>