首先說明線程中要回收哪些資源,理解清楚了這點之后在思考資源回收的問題。
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("清理函數\n"); }
線程主動清理過程的嚴謹寫法:
void thread_fun(void*p) {
p=malloc(20); pthread_cleanup_push(cleanup,p); printf("子線程\n"); sleep(1); //系統調用,用來響應pthread_cancel函數 printf("是否殺死了線程\n"); //如果線程在上一句被殺死,這一句不會被打印 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("子線程\n"); sleep(1); //系統調用,用來響應pthread_cancel函數 printf("是否殺死了線程\n"); //如果線程在上一句被殺死,這一句不會被打印 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。