You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

pthread TLS析构函数释放内存未被valgrind/massif检测到问题

问题现象

创建带自定义析构函数的TLS键后,启动大量线程并将该TLS键传递给每个线程使用:每个线程自行分配堆内存,将内存指针存入TLS槽位,约定由TLS析构函数负责释放该段内存,且应用退出前会通过pthread_join等待所有线程执行完成。
但使用valgrind/massif运行该应用时,工具报告上述TLS中存储的内存未被释放。

复现代码

主函数逻辑:

int main(int argc, char **argv)
{
  pthread_key_t* key = new pthread_key_t();
  pthread_key_create(key, my_destructor);

  pthread_t threads[32000];

  for(int i=0; i<32000; ++i)
    pthread_create(&threads[i], NULL, my_thread, key);

  for(int i=0; i<32000; ++i)
    pthread_join(threads[i], NULL);

  return 0;
}

线程执行函数(分配内存并存入TLS):

extern "C" void* my_thread(void* p)
{
  pthread_setspecific(*(pthread_key_t*)p, malloc(100));

  return NULL;
}

TLS析构函数(释放内存):

extern "C" void my_destructor(void *p)
{
  free(p);
}

运行环境与参数

  • 系统环境:Red Hat Enterprise Linux Server release 7.9 (Maipo)
  • 编译器:gcc 4.9.4
  • valgrind版本:3.19
  • valgrind启动参数:
--tool=massif
  --heap=yes
  --pages-as-heap=yes
  --log-file=/tmp/my.log
  --massif-out-file=/tmp/my.massif.log

通过ms_print /tmp/my.massif.log分析输出文件,得到如下类似内存占用报告:

| ->01.75% (67,108,864B) 0x76F92D0: new_heap (in /usr/lib64/libc-2.17.so)
| | ->01.75% (67,108,864B) 0x76F98D3: arena_get2.isra.3 (in /usr/lib64/libc-2.17.so)
| |   ->01.75% (67,108,864B) 0x76FF77D: malloc (in /usr/lib64/libc-2.17.so)
| |     ->01.75% (67,108,864B) 0x410300: my_thread (threadsT.cpp:136)
| |       ...
| |       <skipped by author>
| |       ...
| |             
| ->00.00% (73,728B) in 1+ places, all below ms_print's threshold (01.00%)

已确认事实

通过在析构函数中添加插桩日志,已经验证:

  • TLS析构函数确实被正常调用
  • 析构函数按照预期逻辑完成了对应内存块的释放操作

问题解答

你的业务代码不存在实际的内存泄漏,这个报告是统计口径和glibc内存分配器缓存机制共同导致的统计偏差,不是真泄漏,也不是valgrind识别不到TLS析构函数里的free操作。
具体原因如下:

  1. 你启动时加了--pages-as-heap=yes参数,这个参数下massif不会只统计应用层申请后未释放的内存,而是会把进程地址空间内所有通过mmap映射、属于堆范畴的内存页全部算成已占用内存。
  2. RHEL7.9自带的glibc 2.17使用ptmalloc2内存分配器,默认会给每个线程创建独立的内存分配arena,每个arena会通过mmap申请大块的内存页(你看到的67108864B刚好是64MB,就是这类arena的典型块大小)。当线程退出时,ptmalloc不会把这些arena占用的内存页通过munmap归还给操作系统,而是会把arena放到内部的空闲链表中缓存,留给后续新建的线程复用。
  3. 你调用free释放的那100字节,确实已经被标记为空闲、可以被后续分配复用,但这部分内存所在的arena整块都还被ptmalloc持有、没有还给OS,所以开了--pages-as-heap=yes的massif会把这整块64MB的内存都算成已占用,回溯栈自然会落到你最初调用malloc的位置。

验证方法非常简单:把启动参数里的--pages-as-heap=yes去掉,再重新跑一次massif,你会看到这64MB的统计项直接消失——默认模式下massif只会统计应用层未调用free的内存,不会把分配器缓存的空闲页算成泄漏。

另外提两个代码里的小瑕疵,和你看到的64MB报告无关,但不符合规范:

  • 你在main里用new创建的pthread_key_t对象从来没有调用delete释放,会产生8字节的极小内存泄漏
  • 所有线程执行完成后,你没有调用pthread_key_delete销毁创建的TLS键,虽然进程退出时OS会自动回收所有资源,但属于不严谨的写法

内容的提问来源于stack exchange,提问作者nsh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 14:19:19