多线程应用程序开发指南:避免堆争用

多线程应用程序中,系统运行时库用于同步对堆的访问。这种锁的竞争可能会限制多线程带来的性能提升。为了解决这个问题,可以采用避免使用共享锁的分配策略,或者使用第三方堆管理器。

系统堆(例如由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; }
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485