检测和避免 POSIX 线程内存泄漏的技巧

POSIX 线程(pthread)编程定义了一套标准的 C 编程语言类型、函数和常量 — 且 pthreads 提供了一种强大的线程管理工具。要充分使用 pthreads,您要避免常见错误。一个常见的错误就是忘记联接可接合的线程,从而导致内存泄漏并增加工作量。在该篇技巧型文章中,学习 POSIX 线程基础,了解如何识别和检测线程内存泄漏,并获得避免出现这种情况的可靠建议。

POSIX 线程简介

使用线程的主要原因是要提高程序性能。线程的创建和管理只需要较小的操作系统开销和较少的系统资源。一个进程内的所有线程共享相同的地址空间,使得线程间的通信更高效,且比进程间通信更易于实现。例如,如果一个线程在等待一个输入/输出系统调用完成,其他线程可以处理 CPU 密集型任务。通过线程,可以优先调度重要任务 — 甚至中断 — 低优先级任务。可将偶尔发生的任务放在定期调度的任务之间,创建调度灵活性。最后,pthreads 是在多 CPU 计算机上进行并行编程的理想之选。

而且使用 POSIX 线程或 pthreads 的主要原因更加简单:作为标准 C 语言线程编程接口的一部分,它们可高可移植的。

POSIX 线程编程有诸多优势,但是如果您不明确一些基本规则,就有可能编写一些难以调试的代码并造成内存泄漏。我们首先回顾一下 POSIX 线程,分为可接合线程 或分离线程

可接合线程

如果您希望生成一个新的线程,且需要知道它是如何终止的,那么您需要一个可接合线程。对于可接合线程,系统分配专用存储器来存储线程终止状态。线程终止后状态得到更新。要获得线程终止状态,调用 pthread_join(pthread_t thread, void** value_ptr)

系统为每个线程分配底层存储,包括堆栈、线程 ID、线程终止状态等。这个底层存储将一直保留在进程空间(且不能回收),直至线程终止并为其他线程所联接。

分离线程

大多数时候,您只需创建一个线程,向其分配一些任务,然后继续处理其他事务。在这些情况下,您不关注线程是如何终止的,这时使用分离线程是一个很好的选择。

对于分离线程,在线程终止后系统自动回收其底层资源。

 

回页首

识别泄漏

如果您创建一个可接合的线程,但是忘记联接它,其资源或私有内存一直保存在进程空间中,从未进行回收再利用。一定要联接可接合的线程;否则,可能会引起严重的内存泄漏问题。

例如,Red Hat Enterprise Linux (RHEL4)上的一个线程需要一个 10MB 的堆栈,这意味着,如果不联接它,会有至少 10MB 的内存泄漏。假设您设计一个管理器-工作线程模式的程序来处理传入的请求。然后需要创建越来越多的工作线程来执行各个任务,最后终止这些线程。如果它们是可接合的线程,且您没有调用 pthread_join() 来联接它们,那么在线程终止后,每个产生的线程都将泄漏大量的内存(至少每堆栈 10MB)。随着创建并在未联接的情况下终止的工作线程越来越多,泄漏的内存量也持续增加。另外,进程将无法创建新的线程,因为无内存可供创建新线程使用。

清单 1 显示在忘记联接可接合线程时引发的严重内存泄漏。您还可以使用该代码检查可在一个进程空间中共存的线程体的最大量。

清单 1. 引发内存泄漏
#include<stdio.h>
#include<pthread.h>
void run() {
pthread_exit(0);
} int main () {
pthread_t thread;
int rc;
long count = 0;
while(1) {
if(rc = pthread_create(&thread, 0, run, 0) ) {
printf("ERROR, rc is %d, so far %ld threads created\n", rc, count);
perror("Fail:");
return -1;
}
count++;
}
return 0;
}

清单 1 中调用了 pthread_create() 来创建一个含默认线程属性的新线程。默认情况下,新创建的线程是可接合的。它不断创建新的可接合线程,直至有故障发生。然后输出错误代码和故障原因。

使用以下命令在 Red Hat Enterprise Linux Server 5.4 上编译清单 1 中的代码时:[root@server ~]# cc -lpthread thread.c -o thread, 您将获得清单 2 所示的结果。

清单 2. 内存泄漏结果
[root@server ~]# ./thread
ERROR, rc is 12, so far 304 threads created
Fail:: Cannot allocate memory

在代码创建了 304 个线程之后,它无法创建更多线程。错误代码是 12,这表示无更多内存可用。

如清单 1 和清单 2 所示,虽然生成了可接合线程,但是却未将其联接,因此每个终止的可接合线程仍然占用进程空间,泄漏进程内存。

RHEL 上的一个 POSIX 线程拥有一个大小为 10MB 的私有堆栈。换言之,系统为每个 pthread 分配至少 10MB 的专用存储。在我们的示例中,304 个线程是在进程停止前创建的;这些线程占用 304*10MB 内存,合计约 3GB。一个进程的虚拟内存的大小是 4GB,其中四分之一的进程空间是为 Linux 内核预留的。这样一来,就有 3GB 的内存空间可用作用户空间。因此,3GB 内存由死线程消耗。这是很严重的内存泄漏。而且很容易理解它发生的速度为何如此之快。

要修复泄漏,您可以添加代码调用 pthread_join(),该方法可联接每个可接合线程。

检测泄漏

如同其他内存泄漏中一样,进程启动时问题可能没那么明显。这里介绍一种无需访问源代码便可检测此类问题的方法:

  1. 计算进程中线程堆栈的数量。这包括正在运行的活动线程和已终止线程的数量。
  2. 计算进程中正在运行的活动线程的数量。
  3. 比较两者。如果现有线程堆栈的数量大于正在运行的活动线程的数量,且在程序运行时这两个数字的差量在不断增加,那么内存在泄漏。

这种内存泄漏很有可能是因未能联接可接合线程而造成的。

使用 pmap 计算线程堆栈数

在一个运行的进程中,线程堆栈的数量等于进程中线程体的数量。线程体包括运行的活动线程和可接合的死线程。

pmap 是一种用于汇报进程内存的 Linux 工具。结合使用以下命令来获取线程堆栈数:

[root@server ~]# pmap PID | grep 10240 | wc -l

(10240KB 是 Red Hat Enterprise Linux Server 5.4 上的默认堆栈大小。)

使用 /proc/PID/task 计算活动线程数

每次创建一个线程且该线程在运行时,会有一个条目填充到 /proc/PID/task 中。当线程终止时,不管该线程是可接合的还是分离的,都会将该条目从 /proc/PID/task 中删除。因此活动线程数可通过运行以下命令得出:

[root@server ~]# ls /proc/PID/task | wc -l.

比较输出

检查 pmap PID | grep 10240 | wc -l 的输出并将其与 ls /proc/PID/task | wc -l 的输出进行比较。如果所有线程堆栈的数量大于活动线程的数量,且在程序运行时两者的差量在持续增长,您可以确定内存泄漏问题确实存在。

 

回页首

预防泄漏

在编程过程中应当联接可接合线程。如果您在程序中创建可接合的线程,切勿忘记调用pthread_join(pthread_t, void**) 来回收分配给线程的专用存储。否则,将引发严重的内存泄漏问题。

在编程后的测试阶段,您可以使用 pmap 和 /proc/PID/task 检测这种泄漏是否存在。如果确实存在,检查源代码,看是否联接了所有可接合线程。

就这些内容。只需少量预防工作即可为您省掉大量后续工作,避免令人头疼的内存泄漏问题。

在 POSIX 线程编程中避免内存泄漏的更多相关文章

  1. 在 JNI 编程中避免内存泄漏

    JAVA 中的内存泄漏 JAVA 编程中的内存泄漏,从泄漏的内存位置角度可以分为两种:JVM 中 Java Heap 的内存泄漏:JVM 内存中 native memory 的内存泄漏. Java H ...

  2. 在 JNI 编程中避免内存泄漏与崩溃

    JNI 编程简介 JNI,Java Native Interface,是 native code 的编程接口.JNI 使 Java 代码程序可以与 native code 交互——在 Java 程序中 ...

  3. Posix线程编程指南(4) 线程终止

    线程终止方式 一般来说,Posix的线程终止有两种情况:正常终止和非正常终止.线程主动调用pthread_exit()或者从线程函数中return都将使线程正常退出,这是可预见的退出方式:非正常终止是 ...

  4. Posix线程编程指南

    Posix线程编程指南 Posix线程编程指南... 1 一线程创建与取消... 2 线程创建... 2 1.线程与进程... 2 2. 创建线程... 2 3. 线程创建属性... 2 4. 创建的 ...

  5. Posix线程编程指南(4)

    Posix线程编程指南(4) 杨沙洲 原文地址:http://www.ibm.com/developerworks/cn/linux/thread/posix_threadapi/part4/ 线程终 ...

  6. Java中关于内存泄漏出现的原因以及如何避免内存泄漏

    转账自:http://blog.csdn.net/wtt945482445/article/details/52483944 Java 内存分配策略 Java 程序运行时的内存分配策略有三种,分别是静 ...

  7. Posix线程编程指南(1)

    这是一个关于Posix线程编程的专栏.作者在阐明概念的基础上,将向您详细讲述Posix线程库API.本文是第一篇将向您讲述线程的创建与取消.   一.线程创建 1.1 线程与进程相对进程而言,线程是一 ...

  8. 关于Hash集合以及Java中的内存泄漏

    <学习笔记>关于Hash集合以及Java中的内存泄漏 标签: 学习笔记内存泄露hash 2015-10-11 21:26 58人阅读 评论(0) 收藏 举报  分类: 学习笔记(5)  版 ...

  9. 系统剖析Android中的内存泄漏

    [转发]作为Android开发人员,我们或多或少都听说过内存泄漏.那么何为内存泄漏,Android中的内存泄漏又是什么样子的呢,本文将简单概括的进行一些总结. 关于内存泄露的定义,我可以理解成这样 没 ...

随机推荐

  1. python学习之判断和循环的使用

    作为一个小白运维,工作中常常发现很多东西还是自动化的好一点,所以就想到的用python来编写脚本.当然,我肯定是不会的啦,哈哈哈~~~~所以啦,身为一个懒癌晚期的上班族不得不在闲余时间来好好学学pyt ...

  2. LeetCode之Weekly Contest 102

    第一题:905. 按奇偶校验排序数组 问题: 给定一个非负整数数组 A,返回一个由 A 的所有偶数元素组成的数组,后面跟 A 的所有奇数元素. 你可以返回满足此条件的任何数组作为答案. 示例: 输入: ...

  3. python 程序小测试

    python 程序小测试 对之前写的程序做简单的小测试 ... # -*- encoding:utf-8 -*- ''' 对所写程序做简单的测试 @author: bpf ''' def GameOv ...

  4. 补之前 如何改变jupyter打开文件的路径

    目录 如何改变jupyter打开文件的路径 第一种方法: 第二种方法 第三种方法 如何改变jupyter打开文件的路径 当我们直接打开jupyter时,直接加载的是我们的C盘文件 现在我们想打开其他盘 ...

  5. Python9-网络编程-day30

    # 由于不同机器上的程序要通信,才产生了网络# server# client# 端口 找到的程序# 在计算机上,每一个需要网络通信的程序,都会开一个端口# 在同一时间只会有一个程序占用一个端口# 不可 ...

  6. The 2018 ACM-ICPC China JiangSu Provincial Programming Contest I. T-shirt

    JSZKC is going to spend his vacation! His vacation has N days. Each day, he can choose a T-shirt to ...

  7. poj 2251 三维地图最短路径问题 bfs算法

    题意:给你一个三维地图,然后让你走出去,找到最短路径. 思路:bfs 每个坐标的表示为 x,y,z并且每个点都需要加上时间 t struct node{ int x, y, z; int t;}; b ...

  8. 求数组中两两相加等于20的组合(Python实现)

    题目 求数组中两两相加等于20的组合. 例:给定一个数组[1, 7, 17, 2, 6, 3, 14],这个数组中满足条件的有两对:17+3=20, 6+14=20. 解析 分为两个步骤: 先采用堆排 ...

  9. UVa 12235 状压DP Help Bubu

    题解戳这 一开始没看懂题解,后来想明白以后,d(i, j, s, x)是考虑第i本书的时候,前面已经拿走了j本书,剩下的书的种类的二进制状态为s,剩下的最后一本书的编号为x,所能得到的最小混乱度. 这 ...

  10. django 缓存 实现

    由于Django构建得是动态网站,每次客户端请求都要严重依赖数据库,当程序访问量大时,耗时必然会更加明显,最简单解决方式是使用:缓存,缓存将一个某个views的返回值保存至内存或者memcache中, ...