首先说明线程中要回收哪些资源,理解清楚了这点之后在思考资源回收的问题。
1、子线程创建时从父线程copy出来的栈内存;
线程退出有多种方式,如return,pthread_exit,pthread_cancel等;线程分为可结合的(joinable)和 分离的(detached)两种,如果没有在创建线程时设置线程的属性为PTHREAD_CREATE_DETACHED,则线程默认是可结合的。可结合的线程在线程退出后不会立即释放资源,必须要调用pthread_join来显式的结束线程。分离的线程在线程退出时系统会自动回收资源。
对于这类资源,主要通过 【设置分离属性 】和 【 pthread_join()】 两种方法来处理。
其中设置分离属性又可以分别用【pthread_attr_setdetachstat()】和【pthread_detach()】来处理。
2、子线程内部单独申请的堆内存(malloc、realloc、calloc)和锁资源mutex;
一旦又处于挂起状态的取消请求(即加锁之后,解锁之前),线程在执行到取消点时如果只是草草收场,这会将共享变量以及pthreads对象(例如互斥量)置于一种不一致状态,可能导致进程中其他线程产生错误结果、死锁,甚至造成程序崩溃。为避免这一问题:
使用清理函数pthread_cleanup_push()和pthread_cleanup_pop()来处理。
线程退出和资源回收
线程退出有多种方式,如return,pthread_exit,pthread_cancel等;线程分为可结合的(joinable)和 分离的(detached)两种,如果没有在创建线程时设置线程的属性为PTHREAD_CREATE_DETACHED,则线程默认是可结合的。可结合的线程在线程退出后不会立即释放资源,必须要调用pthread_join来显式的结束线程。分离的线程在线程退出时系统会自动回收资源。
一、设置分离线程的几种方法:
1.在创建线程时加上
pthread_attr_t attr;
pthread_t thread;
pthread_attr_init (&attr);
/* 设置线程的属性为分离的 */
pthread_attr_setdetachstat(&attr, PTHREAD_CREATE_DETACHED);
pthread_create (&thread, &attr, &thread_function, NULL);
/* 销毁一个目标结构,并且使它在重新初始化之前不能重新使用 */
pthread_attr_destroy (&attr);
2.在线程中调用pthread_detach(pthread_self());
3.主线程中调用pthread_detach(pid),pid为子线程的线程号
要注意的是,设置为分离的线程是不能调用pthread_join的,调用后会出错
二、可结合的线程的几种退出方式
1. 子线程使用return退出,主线程中使用pthread_join回收线程
2.子线程使用pthread_exit退出,主线程中使用pthread_join接收pthread_exit的返回值,并回收线程
3.主线程中调用pthread_cancel,然后调用pthread_join回收线程
注意:在要杀死额子线程对应的处理函数的内部
pthread_cancel函数执行的条件:
1、产生了系统调用(sleep、read、write、open等系统接口)
2、pthread_testcancel();//设置取消点
线程属性结构如下:
typedef struct { int detachstate; //线程的分离状态 int schedpolicy; // 线程调度策略 structsched_param schedparam; //线程的调度参数 int inheritsched; //线程的继承性 int scope; //线程的作用域 size_t guardsize; //线程栈末尾的警戒缓冲区大小 int stackaddr_set; void* stackaddr; //线程栈的位置 size_t stacksize; //线程栈的大小 }pthread_attr_t;
pthread_create 创建线程时,若不指定分配堆栈大小,系统会分配默认值,查看默认值方法如下:
# ulimit -s
8192
#
上述表示为8M;单位为KB。
也可以通过# ulimit -a 其中 stack size 项也表示堆栈大小。ulimit -s value 用来重新设置stack 大小。
一般来说 默认堆栈大小为 8388608; 堆栈最小为 16384 。 单位为字节。
堆栈最小值定义为 PTHREAD_STACK_MIN ,包含#include <limits.h>后可以通过打印其值查看。对于默认值可以通过pthread_attr_getstacksize (&attr, &stack_size); 打印stack_size来查看。
尤其在嵌入式中内存不是很大,若采用默认值的话,会导致出现问题,若内存不足,则 pthread_create 会返回 12,定义如下:
#define EAGAIN 11
#define ENOMEM 12 /* Out of memory */
上面了解了堆栈大小,下面就来了解如何使用 pthread_attr_setstacksize 重新设置堆栈大小。先看下它的原型:
#include <pthread.h>
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
attr 是线程属性变量;stacksize 则是设置的堆栈大小。 返回值0,-1分别表示成功与失败。
这里是使用方法
pthread_t thread_id; int ret ,stacksize = 20480; /*thread 堆栈设置为20K,stacksize以字节为单位。*/ pthread_attr_t attr; ret = pthread_attr_init(&attr); /*初始化线程属性*/
if (ret != 0)return -1; ret = pthread_attr_setstacksize(&attr, stacksize); if(ret != 0)return -1; ret = pthread_create (&thread_id, &attr, &func, NULL); if(ret != 0)return -1; ret = pthread_attr_destroy(&attr); /*不再使用线程属性,将其销毁*/ if(ret != 0)return -1;
在写网络服务器程序时可能需要实现多线程接收多个客户端的数据,我实现方式比较傻,死循环等待client的connect,connect之后创建thread,这样其实有一个问题,服务器程序需要长期运行,长时间线程的创建,线程资源的回收就是一个问题。
Linux系统中程序的线程资源是有限的,表现为对于一个程序其能同时运行的线程数是有限的。而默认的条件下,一个线程结束后,其对应的资源不会被释放,于是,如果在一个程序中,反复建立线程,而线程又默认的退出,则最终线程资源耗尽,进程将不再能建立新的线程。
解决这个问题,有2种方式,系统自动释放线程资源,或者由另一个线程释放该线程资源。
进程运行后,本身,也是一个线程,主线程,主线程和主线程建立的线程共享进程资源。不同于其他线程,在于主线程运行结束后,程序退出,所有程序建立的线程也会退出。
一 系统自动释放
如果想在线程结束时,由系统释放线程资源,则需要设置线程属性为detach,是线程分离主线程
代码上,可以这样表示:
pthread_t t;
pthread_attr_t a; //线程属性
pthread_attr_init(&a); //初始化线程属性
pthread_attr_setdetachstate(&a, PTHREAD_CREATE_DETACHED); //设置线程属性
pthread_create( &t, &a, GetAndSaveAuthviewSDRStub, (void*)lp); //建立线程
二 由另一个线程将该资源释放
代码上,可以这样表示:
pthread_t t;
pthread_create( NULL, NULL, GetAndSaveAuthviewSDRStub, (void*)lp);
pthread_join( t);
pthread_join( t)等待线程t退出,并释放t线程所占用的资源。
pthread_join函数会阻塞等待指定线程退出,然后回收资源,这样就有同步的功能,使一个线程等待另一个线程退出,然后才继续运行,但是对于服务器程序如果主线程在新创建的线程工作时还需要做别的事情,这种方法不是很好,就需要使用方法一
linux线程执行和windows不同,pthread有两种状态joinable状态和unjoinable状态,如果线程是joinable状态,当线程函数自己返回退出时或pthread_exit时都不会释放线程所占用堆栈和线程描述符(总计8K多)。只有当你调用了pthread_join之后这些资源才会被释放。
若是unjoinable状态的线程,这些资源在线程函数退出时或pthread_exit时自动会被释放。
unjoinable属性可以在pthread_create时指定,或在线程创建后在线程中pthread_detach自己,如:pthread_detach(pthread_self()),将状态改为unjoinable状态,确保资源的释放。或者将线程置为joinable,然后适时调用pthread_join.
还有2个函数可以实现线程的分离,pthread_detach(threadid)和pthread_detach(pthread_self())。
这2个函数区别是调用他们的线程不同,没其他区别。
pthread_detach(threadid)函数的功能是使线程ID为threadid的线程处于分离状态,一旦线程处于分离状态,该线程终止时底层资源立即被回收;否则终止子线程的状态会一直保存(占用系统资源)直到主线程调用pthread_join(threadid,NULL)获取线程的退出状态。
通常是主线程使用pthread_create()创建子线程以后,一般可以调用pthread_detach(threadid)分离刚刚创建的子线程,这里的threadid是指子线程的threadid;如此以来,该子线程止时底层资源立即被回收;
被创建的子线程也可以自己分离自己,子线程调用pthread_detach(pthread_self())就是分离自己,因为pthread_self()这个函数返回的就是自己本身的线程ID。
资源清理
一旦又处于挂起状态的取消请求(即加锁之后,解锁之前),线程在执行到取消点时如果只是草草收场,这会将共享变量以及pthreads对象(例如互斥量)置于一种不一致状态,可能导致进程中其他线程产生错误结果、死锁,甚至造成程序崩溃。为避免这一问题:
使用清理函数pthread_cleanup_push()和pthread_cleanup_pop()来处理,这两个函数必需成对出现,不然会编译错误。
不论是可预见的线程种植还是异常终止,都会存在资源释放的问题,在不考虑因运行出错而退出的前提下,如何保证种植时能顺利的释放掉自己所占用的资源,包括单独申请的对内存,特别是锁资源,就是一个必需考虑的问题。
最近常出现的情形时资源独占锁的使用:线程为了访问临界共享资源而为其加上锁,但在访问过程呗外界取消,或者发生了中断,则该临界资源将永远处于锁定状态得不到释放。外界取消操作是不可见的,因此的确需要一个机制来简化用于资源释放的编程。
在POSIX线程API中提供了一个pthread_clean_push()/pthread_cleanup_pop()函数对,用于自动释放资源----从pthread_cleanup_push()的调用点到pthread_cleanup_pop()之间的程序段中的终止动作都将执行pthread_cleanup_push()所指定的清理函数。
API定义如下:
void pthread_cleanup_push(void (*routine) (void *), void *arg) void pthread_cleanup_pop(int execute)
pthread_cleanup_push()/pthread_cleanup_pop()采用先入后出的栈结构管理,
void routine(void *arg)函数在调用pthread_cleanup_push()时压入清理函数栈,多次对pthread_cleanup_push()的调用将在清理函数栈中形成一个函数链,在执行该函数链时按照压栈的相反顺序弹出。execute参数表示执行到pthread_cleanup_pop()时是否在弹出清理函数的同时执行该函数,为0表示不执行,非0为执行;这个参数并不影响异常终止时清理函数的执行。
有三种情况线程清理函数会被调用:
- 线程还未执行 pthread_cleanup_pop 前,被 pthread_cancel 取消
- 线程还未执行 pthread_cleanup_pop 前,主动执行 pthread_exit 终止
- 线程执行 pthread_cleanup_pop,且 pthread_cleanup_pop 的参数不为 0.
注意:如果线程还未执行 pthread_cleanup_pop 前通过 return 返回,是不会执行清理函数的。
void routine()函数可参照如下定义:
void *cleanup(void* p) { free(p); printf("清理函数 "); }
线程主动清理过程的严谨写法:
void thread_fun(void*p) {
p=malloc(20); pthread_cleanup_push(cleanup,p); printf("子线程 "); sleep(1); //系统调用,用来响应pthread_cancel函数 printf("是否杀死了线程 "); //如果线程在上一句被杀死,这一句不会被打印 pthread_exit(NULL); //不管线程是否被杀死,这一句都会检测清理函数,并执行 pthread_clean_pop(1); }
注意:在子线程中如果申请了单独的堆空间,不应用free直接清理;因为假如在线程中直接free,如果,在free之后线程被取消,清理函数被执行,则会出现重复free的情况。
情况如下:
void thread_fun(void*p) { p=malloc(20); pthread_cleanup_push(cleanup,p); printf("子线程 "); sleep(1); //系统调用,用来响应pthread_cancel函数 printf("是否杀死了线程 "); //如果线程在上一句被杀死,这一句不会被打印 free(p); //不管线程是否被杀死,这一句都会检测清理函数,并执行
//加入函数在此处被cancel ,可能会出现重复free
sleep(1);//在此处系统调用,响应pthread_cancel(),会执行清理函数
return NULL;
pthread_clean_pop(1);//由于上一句return,所以这一句不执行,即清理函数不会执行 }
pthread_cleanup_push()/pthread_cleanup_pop()是以宏方式实现的,这是pthread.h中的宏定义:
#define pthread_cleanup_push(routine,arg) { struct _pthread_cleanup_buffer _buffer; _pthread_cleanup_push (&_buffer, (routine), (arg)); #define pthread_cleanup_pop(execute) _pthread_cleanup_pop (&_buffer, (execute)); }
可见,pthread_cleanup_push()带有一个"{",而pthread_cleanup_pop()带有一个"}",因此这两个函数必须成对出现,且必须位于程序的同一级别的代码段中才能通过编译。
在下面的例子里,当线程在"do some work"中终止时,将主动调用pthread_mutex_unlock(mut),以完成解锁动作。
以下是使用方法:
pthread_cleanup_push(pthread_mutex_unlock, (void *) &mut); pthread_mutex_lock(&mut); ....... pthread_mutex_unlock(&mut); pthread_cleanup_pop(0);
必须要注意的是,如果线程处于PTHREAD_CANCEL_ASYNCHRONOUS状态,上述代码段就有可能出错,因为CANCEL事件有可能在
pthread_cleanup_push()和pthread_mutex_lock()之间发生,或者在pthread_mutex_unlock()和pthread_cleanup_pop()之间发生,从而导致清理函数unlock一个并没有加锁的
mutex变量,造成错误。因此,在使用清理函数的时候,都应该暂时设置成PTHREAD_CANCEL_DEFERRED模式。为此,POSIX的
Linux实现中还提供了一对不保证可移植的pthread_cleanup_push_defer_np()/pthread_cleanup_pop_defer_np()扩展函数,功能与以下
代码段相当:
{ int oldtype; pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, &oldtype); pthread_cleanup_push(routine, arg); ...... pthread_cleanup_pop(execute); pthread_setcanceltype(oldtype, NULL); }
补充:
在线程宿主函数中主动调用return,如果return语句包含在pthread_cleanup_push()/pthread_cleanup_pop()对中,则不会引起清理函数的执行,反而会导致segment fault。