线程终止清理函数
一个线程在执行的时候可以被其它线程cancel,也可能遇到程序崩溃等情况造成线程意外退出;假如线程此时申请了堆空间,或者加了锁之后,意外退出造成堆空间或者锁没有被清理,会出现比较严重的后果,尤其是锁!所以当正在运行的线程一旦出现意外退出后该怎么处理呢?-----使用线程终止清理函数!
函数原型:
//这是一对函数,需要成对的使用
void pthread_cleanup_push(void (*routine)(void *), void *arg);
//参数1:线程清理函数
//参数2:线程清理函数的参数
void pthread_cleanup_pop(int execute);
//参数:有两种值可以填,0或者非0值
这对函数对是采用栈结构来管理的清理函数的,调用pthread_cleanup_push()函数会将*routine指向的清理函数入栈,调用pthread_cleanup_pop()函数时,如果参数是非0值,就表示清理函数出栈,参数为0时,就表示不出栈!
代码示例:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
void *threadFunc(void *p)
{
char *p1 = (char *)malloc(20);
strcpy(p1, "nihao");
printf("p1 = %s\n", p1);
free(p1);
p1 = NULL;
printf("p1 free success\n");
pthread_exit(NULL);
}
int main(int argc, char **argv)
{
pthread_t thid;
pthread_create(&thid, NULL, threadFunc, NULL);
//给子线程发送cancel信号
pthread_cancel(thid);
printf("main thread\n");
pthread_join(thid, NULL);
return 0;
}
//运行结果
main thread
p1 = nihao
显然上面的代码出现了内存泄露问题!下面使用线程终止清理函数来解决该问题!
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
void cleanFunc(void *p)
{
printf("cleanfunc\n");
free(p);
p = NULL;
}
void *threadFunc(void *p)
{
char *p1 = (char *)malloc(20);
strcpy(p1, "nihao");
//将清理函数cleanFunc入栈,并且将需要清理的资源传入进去
pthread_cleanup_push(cleanFunc, p1);
printf("p1 = %s\n", p1);
free(p1);
p1 = NULL;
printf("p1 free success\n");
//清理函数出栈
pthread_cleanup_pop(1);
pthread_exit(NULL);
}
int main(int argc, char **argv)
{
pthread_t thid;
pthread_create(&thid, NULL, threadFunc, NULL);
//给子线程发送cancel信号
pthread_cancel(thid);
printf("main thread\n");
pthread_join(thid, NULL);
return 0;
}
//运行结果:
main thread
p1 = nihao
cleanfunc
此时看到这里,你脑海中应该有个疑问才对!为什么清理函数出栈的操作写在了29行,而线程是在21行被终止的,那么肯定运行不到29行,那么清理函数又是怎么得到执行的呢?
其实线程清理函数的到相应是有三个时机的!只要触发一个时机就会出栈,时机分别是:
- 线程收到cancel信号
- 线程主动调用pthread_cleanup_pop(非零值)
- 调用pthread_exit()函数
刚才的代码显然触发了时机1,收到cancel信号触发清理函数弹栈的!下面我将演示一些另外两个时机!
代码示例:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
void cleanFunc(void *p)
{
printf("cleanfunc\n");
free(p);
p = NULL;
}
void *threadFunc(void *p)
{
char *p1 = (char *)malloc(20);
strcpy(p1, "nihao");
//将清理函数cleanFunc入栈,并且将需要清理的资源传入进去
pthread_cleanup_push(cleanFunc, p1);
printf("p1 = %s\n", p1);
free(p1);
p1 = NULL;
printf("p1 free success\n");
//清理函数出栈
pthread_cleanup_pop(1);
pthread_exit(NULL);
}
int main(int argc, char **argv)
{
pthread_t thid;
pthread_create(&thid, NULL, threadFunc, NULL);
//给子线程发送cancel信号
//注释掉该函数,防止触发线程清理函数出栈的第一个时机
/* pthread_cancel(thid); */
printf("main thread\n");
pthread_join(thid, NULL);
return 0;
}
//运行结果:
main thread
p1 = nihao
p1 free success
cleanfunc
free(): double free detected in tcache 2
已放弃 (核心已转储)
注意我上面代码注释掉了pthread_cancel(),上面代码显然是触发了线程清理函数的执行!下面演示最后一个触发时机!
代码示例:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
void cleanFunc(void *p)
{
printf("cleanfunc\n");
free(p);
p = NULL;
}
void *threadFunc(void *p)
{
char *p1 = (char *)malloc(20);
strcpy(p1, "nihao");
//将清理函数cleanFunc入栈,并且将需要清理的资源传入进去
pthread_cleanup_push(cleanFunc, p1);
printf("p1 = %s\n", p1);
free(p1);
p1 = NULL;
printf("p1 free success\n");
pthread_exit(NULL);
//将显示弹栈的操作放在了pthread_exit函数后面,为了清晰演示调用时机3,我将参数置为0.
pthread_cleanup_pop(0);
}
int main(int argc, char **argv)
{
pthread_t thid;
pthread_create(&thid, NULL, threadFunc, NULL);
//给子线程发送cancel信号
//注释掉该函数,防止触发线程清理函数出栈的第一个时机
/* pthread_cancel(thid); */
printf("main thread\n");
pthread_join(thid, NULL);
return 0;
}
//运行结果
main thread
p1 = nihao
p1 free success
cleanfunc
free(): double free detected in tcache 2
已放弃 (核心已转储)
注意第31行代码我的修改,显然第三个清理时机也看到了!下面我将展示一个令你疑惑的代码!
代码示例:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
void cleanFunc(void *p)
{
printf("cleanfunc\n");
free(p);
p = NULL;
}
void *threadFunc(void *p)
{
char *p1 = (char *)malloc(20);
strcpy(p1, "nihao");
//将清理函数cleanFunc入栈,并且将需要清理的资源传入进去
pthread_cleanup_push(cleanFunc, p1);
printf("p1 = %s\n", p1);
free(p1);
p1 = NULL;
printf("p1 free success\n");
//清理函数出栈
pthread_cleanup_pop(0);
pthread_exit(NULL);
}
int main(int argc, char **argv)
{
pthread_t thid;
pthread_create(&thid, NULL, threadFunc, NULL);
//给子线程发送cancel信号
//注释掉该函数,防止触发线程清理函数出栈的第一个时机
/* pthread_cancel(thid); */
printf("main thread\n");
pthread_join(thid, NULL);
return 0;
}
//运行结果
main thread
p1 = nihao
p1 free success
有童鞋蒙了,与前一个的代码长得一样啊,结果怎么不一样呢?靓仔,仔细看一下两个代码的pthread_cleanup_pop和pthread_exit函数的位置,明显的不一样么!那么为什么pop(0)写在了exit的前面就不会触发线程清理函数的执行呢?这就要讲一下原理了!
- 在调用push函数的时候,清理函数入栈,函数入栈的时候还会带一个标志位,这个标志位一开始位1,当调用pop函数出栈时,系统会检查pop的参数,如果参数为0时,就将函数的标志位置为0!否则不去改变标志位!
- 所以在最后的这个代码片段上,我先写的pop(0),所以提前将函数的标志位置为0,那么当调用exit函数的时候,发现标志位为0,那么就不会触发线程清理函数的出栈的!
清理函数对使用的注意事项:
- 清理函数对必须要成对的使用!
- 需要在同一层次的代码中使用清理函数对!
线程清理函数对的底层实现其实是宏!
define pthread_cleanup_push(routine, arg) \
do { \
struct __pthread_cleanup_frame __clframe \
__attribute__ ((__cleanup__ (__pthread_cleanup_routine))) \
= { .__cancel_routine = (routine), .__cancel_arg = (arg), \
.__do_it = 1 };
define pthread_cleanup_pop(execute) \
__clframe.__do_it = (execute); \
} while (0)
可以清晰的看到pthread_cleanup_push宏实现中do后面只有一个**’{’,pthread_cleanup_pop宏实现中有一个’}’**后面有一个while(0);所以只有这两个宏成对出现的时候才能将{ }匹配成功!若了两个函数不在同一层代码中使用,也会导致大括号匹配出错的情况,造成代码逻辑错,如
//伪代码
void func()
{
while(1){
pthread_cleanup_push();
}
pthread_cleanup_pop();
}
//编译阶段会发生宏替换,想一下push的'{'是不是匹配到了while(1)的右边'}';而pop的'}'匹配到了while(1)的'{';导致代码逻辑错误!
根据经验来看,push函数尽量紧挨着需要清理的资源是比较稳妥的,防止资源与push函数之间发生线程意外退出,造成资源没有清理完成,pop函数一般尽量往后面放,但你可别放到pthread_exit()函数后面啊,放在后面都没有意义啊!
以上就是对线程终止清理函数的一些简单的使用总结,如有错误,望给位大佬不吝指正!如果你通过我的讲解有了收获记得三联哦!原创不易,转载请注明出处!