在多线程应用程序中,系统运行时库用于同步对堆的访问。这种锁的竞争可能会限制多线程带来的性能提升。为了解决这个问题,可以采用避免使用共享锁的分配策略,或者使用第三方堆管理器。
系统堆(例如由malloc使用的堆)是一个共享资源。为了使其能够被多个线程安全地使用,需要添加同步机制来控制对共享堆的访问。同步(在这种情况下是获取锁)需要两次与操作系统的交互(即锁定和解锁),这是一个昂贵的开销。所有内存分配的序列化是一个更大的问题,因为线程花费大量时间等待锁,而不是执行有用的工作。
IntelParallel Amplifier的截图展示了多线程CAD应用程序中的堆争用问题。
图1. 堆分配例程和从中调用的内核函数是瓶颈,消耗了大部分应用程序执行时间。
图2. 堆分配例程中使用的临界区是最受争用的同步对象,导致了大量的等待时间和CPU利用率低下。
Intel®编译器中的OpenMP*实现导出了两个函数:kmp_malloc和kmp_free。这些函数为OpenMP使用的每个线程维护一个线程本地堆,避免了使用保护标准系统堆访问的锁。
Win32* API函数HeapCreate可以用来为应用程序使用的所有线程分配独立的堆。标志HEAP_NO_SERIALIZE用于禁用对这个新堆的同步使用,因为只有一个线程会访问它。堆句柄可以存储在线程本地存储(TLS)位置,以便应用程序线程在需要分配或释放内存时使用这个堆。需要注意的是,以这种方式分配的内存必须由执行分配的相同线程显式释放。
以下示例说明了如何使用上述Win32 API特性来避免堆争用。它使用动态加载库(.DLL)在创建时注册新线程,请求独立管理的未同步堆,并使用TLS记住分配给线程的堆。
#include
static DWORD tls_key;
__declspec(dllexport)
void *
thr_malloc(size_t n)
{
return HeapAlloc(TlsGetValue(tls_key), 0, n);
}
__declspec(dllexport)
void
thr_free(void *ptr)
{
HeapFree(TlsGetValue(tls_key), 0, ptr);
}
// 此示例使用WIN32编程API的几个特性
//
// 它使用.DLL模块来记录线程的创建和销毁。
BOOL WINAPI DllMain(
HINSTANCE hinstDLL,
// DLL模块的句柄
DWORD fdwReason,
// 调用函数的原因
LPVOID lpReserved )
// 保留
{
switch (fdwReason) {
case DLL_PROCESS_ATTACH:
// 使用线程本地存储来记住堆
tls_key = TlsAlloc();
TlsSetValue(tls_key, GetProcessHeap());
break;
case DLL_THREAD_ATTACH:
// 使用HEAP_NO_SERIALIZE避免锁开销
TlsSetValue(tls_key, HeapCreate(HEAP_NO_SERIALIZE, 0, 0));
break;
case DLL_THREAD_DETACH:
HeapDestroy(TlsGetValue(tls_key));
break;
case DLL_PROCESS_DETACH:
TlsFree(tls_key);
break;
}
return TRUE; // 成功的DLL_PROCESS_ATTACH。
}
pthread_key_create和pthread_{get|set}specific API可以用来在应用程序中使用POSIX*线程(Pthreads*)访问TLS,但是没有通用的API来创建独立的堆。可以为每个线程分配一大块内存,并将地址存储在TLS中,但是这种存储的管理是程序员的责任。
除了使用多个独立的堆之外,还可以采用其他技术来最小化共享锁保护系统堆时引起的锁争用。如果内存只在一个小的词法上下文中被访问,有时可以使用alloca例程从当前栈帧分配内存。此内存在函数返回时自动释放。
// 使用malloc()有时可以替换为alloca()
{
…
char *p = malloc(256);
// 使用分配的内存
process(p);
free(p);
…
}
// 如果内存在同一个例程中分配和释放。
{
…
char *p = alloca(256);
// 使用分配的内存
process(p);
…
}
注意,Microsoft已经弃用_alloca,并建议使用安全性增强的例程_malloca。它根据请求的大小,从栈或堆分配内存;因此,从_malloca获得的内存应该用_freea释放。
每个线程的空闲列表是另一种技术。最初,内存是从系统堆中使用malloc分配的。当内存通常会被释放时,它被添加到每个线程的链表中。如果线程需要重新分配相同大小的内存,它可以立即从列表中检索存储的分配,而不必回到系统堆。
struct MyObject {
struct MyObject *next;
…
};
// 每个线程的空闲内存对象列表
static __declspec(thread) struct MyObject *freelist_MyObject = 0;
struct MyObject *
malloc_MyObject( )
{
struct MyObject *p = freelist_MyObject;
if (p == 0)
return malloc(sizeof(struct MyObject));
freelist_MyObject = p->next;
return p;
}
void
free_MyObject(struct MyObject *p )
{
p->next = freelist_MyObject;
freelist_MyObject = p;
}