堆上持续创建pthread的实现与Valgrind内存分析方法
我所开发的程序会创建子线程,希望使用Valgrind memcheck工具对其开展内存分析。根据此前提出的相关问题下的回复,为了通过Valgrind memcheck获得可靠的测试与分析结果,必须使用可合并(joinable)线程,而非分离(detached)线程。
栈分配与堆分配选型
我的程序规模较大,无法在线程创建的同一作用域内完成线程join操作,因此选择在堆上为pthread_t类型变量分配存储空间。
尝试方案1:创建后立即join
#include <stdlib.h> #include <stdio.h> #include <pthread.h> #include <stdint.h> void my_thread() { printf("I'm in a child thread!\n"); pthread_exit(NULL); } pthread_t* const make_thread() { pthread* const thread = malloc(sizeof(pthread_t)); pthread_create(thread, NULL, (void*) &my_thread, NULL)); return thread; } int main() { printf("Hello, world!\n"); uint8_t i; for(i = 0; i < 255; ++i) { pthread_t* const thread_handle = make_thread(); pthread_join(*thread_handle, NULL); free(thread_handle); } return 0; }
该方案逻辑可正常运行,但我希望对示例进行扩展:不在线程创建后立即执行join,仅在程序退出时统一执行join(例如业务场景中线程可能为长生命周期线程),换言之上述串行join的方案无法发挥多线程的并行优势,违背多线程设计初衷。我的目标是创建线程后,仅在程序退出时强制执行join操作。
尝试方案2:程序退出前统一join
#include <stdlib.h> #include <stdio.h> #include <pthread.h> #include <stdint.h> #include <glib-2.0/glib.h> #include <unistd.h> void my_thread() { sleep(3); printf("I'm in a child thread!\n"); pthread_exit(NULL); } pthread_t* const make_thread() { pthread* const thread = malloc(sizeof(pthread_t)); pthread_create(thread, NULL, (void*) &my_thread, NULL)); return thread; } int main() { printf("Hello, world!\n"); GArray* const thread_handles = g_array_new(TRUE, TRUE, sizeof(pthread*)); // 核心业务循环 uint8_t i; for(i = 0; i < 255; ++i) { pthread_t* const thread_handle = make_thread(); g_array_append_val(thread_handles, thread_handle); } for(i = 0; i < thread_handles->len; ++i) { pthread_t* const thread_handle = g_array_index(thread_handles, pthread*, i); pthread_join(*thread_handle, NULL); free(thread_handle); } g_array_free(thread_handles, TRUE); return 0; }
该方案可正常运行,但存在一个待解决的问题:如果上述核心业务循环为无限循环,要如何避免thread_handles数组持续扩张直至耗尽所有可用内存?在我的实际业务程序中(上述代码为最小复现示例),程序会持续接收网络消息,针对特定类型的网络消息启动对应处理线程。现寻求可靠的实现方案,支持在堆上持续创建可join的pthread,兼容Valgrind memcheck内存检测,同时避免无限运行场景下的线程句柄存储内存膨胀问题,实现规范的线程生命周期管理,支撑程序的内存与性能分析。
解决方案
核心思路是定期回收已退出线程的句柄资源,不需要把所有线程句柄都留存到程序退出阶段,只要保证每个joinable线程最终被pthread_join回收、对应堆内存被释放即可,完全兼容Valgrind检测要求,也不会出现内存无限膨胀的问题。
实现方式
- 维护一个线程句柄存储容器(你当前用的GArray即可,也可以用链表、队列结构)
- 每次创建新线程往容器追加句柄后,立刻遍历扫描整个容器:
- 对每个句柄调用
pthread_tryjoin_np(这是glibc提供的非阻塞join接口,若线程已退出则立刻完成回收,未退出则直接返回EBUSY不阻塞) - 对成功join的线程,直接释放对应堆上分配的
pthread_t内存,把该位置从容器中移除
- 对每个句柄调用
- 程序收到退出信号、准备终止前,再对容器中剩余的未退出线程执行阻塞式
pthread_join,等待所有线程执行完成后释放对应资源、销毁容器。
关键注意点
pthread_tryjoin_np是GNU扩展接口,在POSIX标准中没有定义,如果你需要跨POSIX平台兼容,可以额外启动一个低优先级的回收线程,专门对容器中的线程句柄执行阻塞式pthread_join,回收完成后释放内存、从容器删除对应条目即可,效果完全一致。- 对容器的读写操作需要加互斥锁,避免业务线程追加句柄、回收线程扫描删除条目时出现竞态条件。
- 不需要担心扫描容器的性能开销:容器中留存的只会是还在运行的线程句柄,已经退出的线程会被及时清理,容器长度永远等于当前运行中的线程总数,不会无限增长;每次扫描的时间复杂度和当前运行线程数成正比,对业务逻辑的性能影响可以忽略。
- 这种实现方式完全满足Valgrind memcheck的检测要求:所有joinable线程都被显式join,所有堆分配的
pthread_t内存都被显式释放,不会出现"possibly lost"的假阳性误报。
补充说明:分离线程虽然不需要手动join,但是线程退出时其资源的回收时机是由内核调度决定的,Valgrind在程序退出时可能会检测到还未完全回收的线程栈、TLS结构内存,报出假阳性的内存泄漏错误,这也是必须用joinable线程做内存检测的核心原因,上述方案完全规避了这个问题。
内容的提问来源于stack exchange,提问作者karobar

