最近在敲一些线程相关的代码, 线程里有自己分配一些资源, 分配是简单, 但是到了要释放分配的资源那一步 就郁闷了,
要考虑各种异常终止的情况, 代码乱糟糟不说, 容易出问题, 有可能还没释放完全,造成内存泄漏.
下面是一步步的历程.
1. 先给出线程里最原始的需求代码 不加其他异常处理.
-
static int call_sth (char *str)
-
{
-
char *call_res = (char *)calloc(1, 128);
-
-
printf ("call_sth: calloc str: %p\n", call_res);
-
-
// 从外部程序中获取一些数据进行处理
-
// ...
-
-
-
free(call_res);
-
-
return 0;
-
}
-
-
static void *my_func1(void *arg)
-
{
-
char *str = (char *)arg;
-
char *ou = (char *)calloc(1, 128);
-
int ret = 0;
-
-
printf("线程1(ID:0x%x)退出。str(%s) \n",(unsigned int)pthread_self(), str);
-
-
ret = call_sth(str);
-
-
snprintf(ou, 128, "this is my_func1's result: %d\n", ret);
-
-
pthread_exit((void *)ou);
-
}
-
-
typedef void* thread_fun(void *);
-
static pthread_t pthread_start(thread_fun func, void *arg)
-
{
-
pthread_t tid;
-
int ret = 0;
-
-
pthread_attr_t attr;
-
pthread_attr_init(&attr);
-
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
-
-
-
memset(&tid, 0, sizeof(tid));
-
-
ret = pthread_create(&tid, &attr, func, arg);
-
if (ret != 0) {
-
memset(&tid, 0, sizeof(tid));
-
}
-
-
return tid;
-
}
-
-
int my_test(void)
-
{
-
int ret;
-
pthread_t tid;
-
char *res_str = NULL;
-
-
tid = pthread_start(my_func1, "Hello pthread 1 ");
-
if (!pthread_t_is_zero(tid)) {
-
while (1) { //每1秒看一次线程有没有退出
-
if (!pthread_is_alive(tid)) {
-
//线程退出 获取返回值
-
pthread_join(tid, (void **)&res_str);
-
if (res) {
-
if (res == PTHREAD_CANCELED) {
-
printf("ID为0x%x的线程被CANCEL终止\n", (unsigned int)tid);
-
}
-
else {
-
printf("获取ID为0x%x的线程结果: %s\n", (unsigned int)tid, res_str);
-
free(res_str);
-
res_str = NULL;
-
}
-
}
-
memset(&tid, 0, sizeof(tid));
-
}
-
else
-
sleep(1);
-
}
-
}
-
-
return 0;
-
}
从代码可以看出, 很简单的一个线程, 线程里 分配一次空间存储动态数据 并返回.
但是 考虑到如果出现用户退出或其他情况需要中止线程的情况呢
例如:
call_str()函数 正在从外部获取数据的时候 收到pthread_cancel()发出的停止命令,
这里, 如果在free()前面就有cancel point, 那么后面的free()不会执行 线程就被结束了, 更不用说父函数my_func1
最开始分配的ou 也没被释放的问题了.
1. 使用pthread_cleanup_push() pthread_cleanup_pop()
-
static void * my_free(void *arg)
-
{
-
printf(" free %p\n", arg);
-
-
free(arg);
-
}
-
-
static int call_sth (char *str)
-
{
-
char *call_res = (char *)calloc(1, 128);
-
char *call_res1 = (char *)calloc(1, 256);
-
-
printf("call res: %p call_res1: %p\n", call_res, call_res1);
-
pthread_cleanup_push(my_free, call_res);
-
pthread_cleanup_push(my_free, call_res1);
-
-
printf ("call_sth: calloc str: %p\n", call_res);
-
-
// get the data to call_res
-
// ...
-
sleep(5); //为了测试这里睡眠几秒让主线程有时间pthread_cancel()
-
-
pthread_cleanup_pop(call_res);
-
pthread_cleanup_pop(call_res1);
-
-
return 0;
-
}
-
-
static void *my_func1(void *arg)
-
{
-
char *str = (char *)arg;
-
char *ou = (char *)calloc(1, 128);
-
int ret = 0;
-
-
printf("ou: %p\n", ou);
-
pthread_cleanup_push(my_free, (void *)ou);
-
-
printf("线程1(ID:0x%x)退出。str(%s) \n",(unsigned int)pthread_self(), str);
-
-
ret = call_sth(str);
-
-
snprintf(ou, 128, "this is my_func1's result: %d\n", ret);
-
-
pthread_cleanup_pop(0);
-
-
pthread_exit((void *)ou);
-
}
我只粘贴了部分代码, 只是想展示使用pthread_cleanup_push() 和pthread_cleanup_pop() 来防止异常终止 内存泄漏的情况.
可以看到, push() pop()必须成对出现, 而且每有一个动态分配的内存 如果其使用过程中有cancel point 导致其可能内存泄漏 就得使用push() pop()这样处理.
缺点 :
1. 如果分配的地方多, push() pop()将到处都是. 如果你想一个函数来释放所有分配的空间...
嗷, 你又得发时间来弄个分配空间管理程序, 一个函数释放所有分配的空间, 如果你执意如此, 祝你顺利, 可以略过后面内容, 敲代码去吧!
2. 另一方面 通过man看函数说明, 可以看到push() pop() 是以宏的方式来展开的, 所以push() pop()要成对出现.
如果 你在一个循环里面多次分配/重新分配呢 , 直接会导致编译失败 例如下面这样:
-
static int call_sth (char *str)
-
{
-
char *call_res = (char *)calloc(1, 128);
-
char *call_res1 = (char *)calloc(1, 256);
-
char *fuck_p = NULL;
-
int len = 0;
-
-
printf("call res: %p call_res1: %p\n", call_res, call_res1);
-
pthread_cleanup_push(my_free, call_res);
-
pthread_cleanup_push(my_free, call_res1);
-
-
printf ("call_sth: calloc str: %p\n", call_res);
-
-
for (;;) {
-
-
// get the data and len
-
// ...
-
len = 256;
-
-
if (!fuck_p) {
-
fuck_p = (char *) calloc(1, len);
-
printf ("call_sth: calloc fuck_p: %p\n", fuck_p);
-
-
pthread_cleanup_push(my_free, fuck_p); //编译失败 错误
-
}
-
-
sleep(1);
-
}
-
pthread_cleanup_pop(fuck_p);
-
-
pthread_cleanup_pop(call_res);
-
pthread_cleanup_pop(call_res1);
-
-
return 0;
-
}
2. 使用pthread_key_create() pthread_getspecific() pthread_setspecific()
这几个函数是用来保存线程私有数据的, 但是pthread_key_create(pthread_key_t *key, void (*destructor)(void*)) 可以为这个key映射的值指定释放函数.
当线程退出时如果这个key的值不为空 那么会自动调用这个函数. 借助这个我们也可以做一些释放分配内存的操作.
-
static void my_free(void *arg)
-
{
-
printf(" free %p\n", arg);
-
-
free(arg);
-
}
-
static pthread_key_t key[10];
-
static pthread_once_t key_once = PTHREAD_ONCE_INIT;
-
-
static void make_key()
-
{
-
int i = 0;
-
-
for (i=0; i<10; i++) {
-
pthread_key_create(&key[i], my_free);
-
}
-
}
-
-
static int call_sth (char *str)
-
{
-
char *call_res = (char *)calloc(1, 128);
-
char *call_res1 = (char *)calloc(1, 256);
-
char *fuck_p = NULL;
-
int len = 0;
-
-
printf("call res: %p call_res1: %p\n", call_res, call_res1);
-
-
pthread_setspecific(key[1], call_res);
-
pthread_setspecific(key[2], call_res1);
-
-
for (;;) {
-
-
// get the data and len
-
// ...
-
len = 256;
-
-
if (!fuck_p) {
-
fuck_p = (char *) calloc(1, len);
-
printf ("call_sth: calloc fuck_p: %p\n", fuck_p);
-
-
pthread_setspecific(key[3], fuck_p);
-
}
-
-
sleep(1);
-
}
-
-
return 0;
-
}
-
-
static void *my_func1(void *arg)
-
{
-
char *str = (char *)arg;
-
char *ou = (char *)calloc(1, 128);
-
int ret = 0;
-
-
printf("ou = %p\n", ou);
-
-
/* 保证只调用make_key初始化一次 */
-
pthread_once(&key_once, make_key);
-
-
pthread_setspecific(key[0], ou);
-
-
printf("线程1(ID:0x%x)退出。str(%s) \n",(unsigned int)pthread_self(), str);
-
-
ret = call_sth(str);
-
-
snprintf(ou, 128, "this is my_func1's result: %d\n", ret);
-
-
/* 因为要返回这个指针指向的内容,不能释放,
-
* 所以得赋为空 防止退出线程时被自动释放了. */
-
pthread_setspecific(key[0], NULL);
-
-
pthread_exit((void *)ou);
-
}
我只粘贴了部分代码, 只是想展示使用pthread_key_create
() pthread_setspecific() 来防止异常终止 内存泄漏的情况.
可以看到, 这类函数不像上面说的push() pop()那样以宏的方式展开 还需要成队出现, 比较省事. 但缺点同push() pop()类似
缺点 :
1. 事先得知道大概要分配多少次内存 和 内存的数据类型(要用什么释放函数), 根据这些先初始化好key, 如果是其他类型的数据需要其他的函数来释放, 得单独另外的初始化.
例如:
我得到了cJSON *json = cJSON_Parse(buff), 数据, 要为这个json 使用key的话, key必须要初始化释放函数为 cJSON_Delete()才行, 用free肯定是不得行的.
pthread_key_t json_key;
pthread_key_create
(json_key, cJSON_Delete);
pthread_key_setspecific(json_key, json);
2. 容易弄混, 如果要为每一个要用的内存指针 分配一个单独的名字key 太麻烦.
如果直接为同一类型的内存指针 分配一个数组 像上面代码中所述那样, 使用起来容易搞混, 可能这个子函数里用了key[2] 那个子函数里用了key[3], 这样下去会剪不断 理还乱, 崩溃...
3. 不是办法的办法
究其根源, 为什么会有异常中止的情况, 其实大部分情况下 都是正常的流程 直到线程正常终止, 异常中止只是我个人的YY.
大型程序中是可以弄一个内存管理程序的, 线程退出时统一释放, 这里不讨论.
小/中型程序中 如果像上面那样弄得太复杂 极有可能漏掉某个分配的没有处理 导致内存泄漏; 如果业务允许 不用立即退出, 可以让其执行完业务后自动退出, 正常释放, 代码将会简单很多.
例如: 当主线程需要退出的时候, 直接分离正在运行的子线程, 让其走完正常的流程自己释放自己分配空间 执行完后 系统将回收子线程资源, 整个世界都安静了~
注: 如果 要返回一些内容给父线程, 可以退出前先查看父线程是否存在, 不存在的话, 自己释放用来返回给父线程的内存空间 再退出.
省事,简单,有效的方法:
1. 使用 "全局变量" "套接字传数据" 等其他方法 来控制子线程的退出, 而不是直接粗暴的pthread_cancel()
2. 设置子线程 PTHREAD_CANCEL_DISABLE , 不允许被其他线程异常中止.
3. 子线程 自己按照正常逻辑分配 - 使用 - 释放 内存空间 直到线程终止.
完整的代码如下:
-
/*pthread_kill的返回值:成功(0) 线程不存在(ESRCH) 信号不合法(EINVAL)*/
-
enum {
-
PTHREAD_RET_ALIVE = 1,
-
PTHREAD_RET_ESRCH,
-
PTHREAD_RET_EINVAL,
-
};
-
-
static int pthread_test(pthread_t tid)
-
{
-
int pthread_kill_err;
-
pthread_kill_err = pthread_kill(tid,0);
-
-
if(pthread_kill_err == ESRCH) {
-
printf("ID为0x%x的线程不存在或者已经退出。\n",(unsigned int)tid);
-
return PTHREAD_RET_ESRCH;
-
}
-
else if(pthread_kill_err == EINVAL) {
-
printf("发送信号非法。/n");
-
return PTHREAD_RET_EINVAL;
-
}
-
-
printf("ID为0x%x的线程目前仍然存活。\n",(unsigned int)tid);
-
return PTHREAD_RET_ALIVE;
-
}
-
#define pthread_is_alive(tid) (pthread_test(tid) != PTHREAD_RET_ESRCH)
-
-
static inline int pthread_t_is_zero(pthread_t tid)
-
{
-
pthread_t zero_tid;
-
memset(&zero_tid, 0, sizeof(zero_tid));
-
-
return !memcmp(&tid, &zero_tid, sizeof(pthread_t));
-
}
-
-
static int call_sth (char *str)
-
{
-
char *call_res = (char *)calloc(1, 128);
-
char *call_res1 = (char *)calloc(1, 256);
-
char *fuck_p = NULL;
-
int len = 0;
-
-
for (;;) {
-
-
// get the data and len
-
// ...
-
len = 256;
-
-
if (!fuck_p) {
-
fuck_p = (char *) calloc(1, len);
-
}
-
-
sleep(1);
-
}
-
-
if (call_res)
-
free(call_res);
-
if (call_res1)
-
free(call_res1);
-
if (fuck_p)
-
free(fuck_p);
-
-
return 0;
-
}
-
-
typedef struct pthread_struct_t{
-
char * data;
-
pthread_t p_tid;
-
}p_t;
-
-
static void *my_func1(void *arg)
-
{
-
p_t * p = (p_t *)arg;
-
char *str = p->data;
-
char *ou = (char *)calloc(1, 128);
-
int ret = 0;
-
-
/* 不允许pthread_cancel() 终止 */
-
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
-
-
printf("ou = %p\n", ou);
-
-
printf("线程1(ID:0x%x)退出。str(%s) \n",(unsigned int)pthread_self(), str);
-
-
ret = call_sth(str);
-
-
snprintf(ou, 128, "this is my_func1's result: %d\n", ret);
-
-
/* 父线程不在了, 不用返回数据了 释放分配的内存空间 */
-
if (!pthread_is_alive(p->p_tid) && ou) {
-
free(ou);
-
ou = NULL;
-
}
-
-
pthread_exit((void *)ou);
-
}
-
-
typedef void* thread_fun(void *);
-
static pthread_t pthread_start(thread_fun func, void *arg)
-
{
-
pthread_t tid;
-
int ret = 0;
-
-
pthread_attr_t attr;
-
pthread_attr_init(&attr);
-
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
-
-
-
memset(&tid, 0, sizeof(tid));
-
-
ret = pthread_create(&tid, &attr, func, arg);
-
if (ret != 0) {
-
memset(&tid, 0, sizeof(tid));
-
}
-
-
return tid;
-
}
-
-
int my_test(void)
-
{
-
int ret;
-
pthread_t tid;
-
char *res_str = NULL;
-
int count = 0 ;
-
p_t p;
-
-
p.p_tid = pthread_self();
-
p.data = "Hello pthread 1";
-
tid = pthread_start(my_func1, &p);
-
if (!pthread_t_is_zero(tid)) {
-
while (1) {
-
/* 1s过后想停止掉子线程 */
-
if (count > 0) {
-
#if USE_CANCEL
-
printf("cancel pthread[0x%x]\n", (unsigned int)tid);
-
pthread_cancel(tid);
-
sleep(1);
-
#else
-
printf("detach pthread[0x%x]\n", (unsigned int)tid);
-
pthread_detach(tid);
-
break;
-
#endif
-
}
-
-
if (!pthread_is_alive(tid)) {
-
pthread_join(tid, (void **)&res_str);
-
if (res_str) {
-
if (res_str == PTHREAD_CANCELED) {
-
printf("ID为0x%x的线程被CANCEL终止\n", (unsigned int)tid);
-
}
-
else {
-
printf("获取ID为0x%x的线程结果: %s\n", (unsigned int)tid, res_str);
-
free(res_str);
-
res_str = NULL;
-
}
-
}
-
memset(&tid, 0, sizeof(tid));
-
break;
-
}
-
else {
-
sleep(1);
-
count ++;
-
}
-
}
-
}
-
-
return 0;
-
}
再看看这边的代码 一下子清静了...