Linux多執行緒學習
【原文:http://blog.chinaunix.net/uid-1849361-id-2825470.html】
Linux下的多執行緒程式設計
作者: 姚繼鋒 (2001-08-11 09:05:00)
1 引言
執行緒(thread)技術早在60年代就被提出,但真正應用多執行緒到 *** 作系統中去,是在80年代中期,solaris是這方面的佼佼者。傳統的Unix也支援執行緒的概念,但是在一個程式(process)中只允許有一個執行緒,這樣多執行緒就意味著多程式。現在,多執行緒技術已經被許多 *** 作系統所支援,包括Windows/NT,當然,也包括Linux。
為什麼有了程式的概念後,還要再引入執行緒呢?使用多執行緒到底有哪些好處?什麼的系統應該選用多執行緒?我們首先必須回答這些問題。
使用多執行緒的理由之一是和程式相比,它是一種非常"節儉"的多工 *** 作方式。我們知道,在Linux系統下,啟動一個新的程式必須分配給它獨立的地址空間,建立眾多的資料表來維護它的代 *** 段、堆疊段和資料段,這是一種"昂貴"的多工工作方式。而執行於一個程式中的多個執行緒,它們彼此之間使用相同的地址空間,共享大部分資料,啟動一個執行緒所花費的空間遠遠小於啟動一個程式所花費的空間,而且,執行緒間彼此切換所需的時間也遠遠小於程式間切換所需要的時間。據統計,總的說來,一個程式的開銷大約是一個執行緒開銷的30倍左右,當然,在具體的系統上,這個資料可能會有較大的區別。
使用多執行緒的理由之二是執行緒間方便的通訊機制。對不同程式來說,它們具有獨立的資料空間,要進行資料的傳遞只能通過通訊的方式進行,這種方式不僅費時,而且很不方便。執行緒則不然,由於同一程式下的執行緒之間共享資料空間,所以一個執行緒的資料可以直接為其它執行緒所用,這不僅快捷,而且方便。當然,資料的共享也帶來其他一些問題,有的變數不能同時被兩個執行緒所修改,有的子程式中宣告為static的資料更有可能給多執行緒程式帶來災難 *** 的打擊,這些正是編寫多執行緒程式時最需要注意的地方。
除了以上所說的優點外,不和程式比較,多執行緒程式作為一種多工、併發的工作方式,當然有以下的優點:
1) 提高應用程式響應。這對圖形介面的程式尤其有意義,當一個 *** 作耗時很長時,整個系統都會等待這個 *** 作,此時程式不會響應鍵盤、滑鼠、選單的 *** 作,而使用多執行緒技術,將耗時長的 *** 作(time consuming)置於一個新的執行緒,可以避免這種尷尬的情況。
2) 使多CPU系統更加有效。 *** 作系統會保證當執行緒數不大於CPU數目時,不同的執行緒執行於不同的CPU上。
3) 改善程式結構。一個既長又複雜的程式可以考慮分為多個執行緒,成為幾個獨立或半獨立的執行部分,這樣的程式會利於理解和修改。
下面我們先來嘗試編寫一個簡單的多執行緒程式。
2 簡單的多執行緒程式設計
Linux系統下的多執行緒遵循POSIX執行緒介面,稱為pthread。編寫Linux下的多執行緒程式,需要使用標頭檔案pthread.h,連線時需要使用庫libpthread.a。順便說一下,Linux下pthread的實現是通過系統呼叫clone()來實現的。clone()是Linux所特有的系統呼叫,它的使用方式類似fork,關於clone()的詳細情況,有興趣的讀者可以去檢視有關文件說明。下面我們展示一個最簡單的多執行緒程式 example1.c。
/* example.c*/
#include <stdio.h>
#include <pthread.h>
void thread(void)
{
int i;
for(i=0;i<3;i++)
printf("This is a pthread.\n");
}
int main(void)
{
pthread_t id;
int i,ret;
ret=pthread_create(&id,NULL,(void *) thread,NULL);
if(ret!=0){
printf ("Create pthread error!\n");
exit (1);
}
for(i=0;i<3;i++)
printf("This is the main process.\n");
pthread_join(id,NULL);
return (0);
}
我們編譯此程式:
gcc example1.c -lpthread -o example1
執行example1,我們得到如下結果:
This is the main process.
This is a pthread.
This is the main process.
This is the main process.
This is a pthread.
This is a pthread.
再次執行,我們可能得到如下結果:
This is a pthread.
This is the main process.
This is a pthread.
This is the main process.
This is a pthread.
This is the main process.
前後兩次結果不一樣,這是兩個執行緒爭奪CPU資源的結果。上面的示例中,我們使用到了兩個函式, pthread_create和pthread_join,並宣告瞭一個pthread_t型的變數。
pthread_t在標頭檔案/usr/include/bits/pthreadtypes.h中定義:
typedef unsigned long int pthread_t;
它是一個執行緒的識別符號。函式pthread_create用來建立一個執行緒,它的原型為:
extern int pthread_create __P ((pthread_t *__thread, __const pthread_attr_t *__attr,
void *(*__start_routine) (void *), void *__arg));
第一個引數為指向執行緒識別符號的指標,第二個引數用來設定執行緒屬 *** ,第三個引數是執行緒執行函式的起始地址,最後一個引數是執行函式的引數。這裡,我們的函式thread不需要引數,所以最後一個引數設為空指標。第二個引數我們也設為空指標,這樣將生成預設屬 *** 的執行緒。對執行緒屬 *** 的設定和修改我們將在下一節闡述。當建立執行緒成功時,函式返回0,若不為0則說明建立執行緒失敗,常見的錯誤返回代 *** 為EAGAIN和EINVAL。前者表示系統 *** 建立新的執行緒,例如執行緒數目過多了;後者表示第二個引數代表的執行緒屬 *** 值非法。建立執行緒成功後,新建立的執行緒則執行引數三和引數四確定的函式,原來的執行緒則繼續執行下一行代 *** 。
函式pthread_join用來等待一個執行緒的結束。函式原型為:
extern int pthread_join __P ((pthread_t __th, void **__thread_return));
第一個引數為被等待的執行緒識別符號,第二個引數為一個使用者定義的指標,它可以用來儲存被等待執行緒的返回值。這個函式是一個執行緒阻塞的函式,呼叫它的函式將一直等待到被等待的執行緒結束為止,當函式返回時,被等待執行緒的資源被收回。一個執行緒的結束有兩種途徑,一種是象我們上面的例子一樣,函式結束了,呼叫它的執行緒也就結束了;另一種方式是通過函式pthread_exit來實現。它的函式原型為:
extern void pthread_exit __P ((void *__retval)) __attribute__ ((__noreturn__));
唯一的引數是函式的返回代 *** ,只要pthread_join中的第二個引數thread_return不是NULL,這個值將被傳遞給thread_return。最後要說明的是,一個執行緒不能被多個執行緒等待,否則第一個接收到訊號的執行緒成功返回,其餘呼叫pthread_join的執行緒則返回錯誤代 *** ESRCH。
在這一節裡,我們編寫了一個最簡單的執行緒,並掌握了最常用的三個函式pthread_create,pthread_join和pthread_exit。下面,我們來瞭解執行緒的一些常用屬 *** 以及如何設定這些屬 *** 。
3 修改執行緒的屬 ***
在上一節的例子裡,我們用pthread_create函式建立了一個執行緒,在這個執行緒中,我們使用了預設引數,即將該函式的第二個引數設為NULL。的確,對大多數程式來說,使用預設屬 *** 就夠了,但我們還是有必要來了解一下執行緒的有關屬 *** 。
屬 *** 結構為pthread_attr_t,它同樣在標頭檔案/usr/include/pthread.h中定義,喜歡追根問底的人可以自己去檢視。屬 *** 值不能直接設定,須使用相關函式進行 *** 作,初始化的函式為pthread_attr_init,這個函式必須在pthread_create函式之前呼叫。屬 *** 物件主要包括是否繫結、是否分離、堆疊地址、堆疊大小、優先順序。預設的屬 *** 為非繫結、非分離、預設1M的堆疊、與父程式同樣級別的優先順序。
關於執行緒的繫結,牽涉到另外一個概念:輕程式(LWP:Light Weight Process)。輕程式可以理解為核心執行緒,它位於使用者層和系統層之間。系統對執行緒資源的分配、對執行緒的控制是通過輕程式來實現的,一個輕程式可以控制一個或多個執行緒。預設狀況下,啟動多少輕程式、哪些輕程式來控制哪些執行緒是由系統來控制的,這種狀況即稱為非繫結的。繫結狀況下,則顧名思義,即某個執行緒固定的"綁"在一個輕程式之上。被繫結的執行緒具有較高的響應速度,這是因為CPU時間片的排程是面向輕程式的,繫結的執行緒可以保證在需要的時候它總有一個輕程式可用。通過設定被繫結的輕程式的優先順序和排程級可以使得繫結的執行緒滿足諸如實時反應之類的要求。
設定執行緒繫結狀態的函式為pthread_attr_setscope,它有兩個引數,第一個是指向屬 *** 結構的指標,第二個是繫結型別,它有兩個取值:PTHREAD_SCOPE_SYSTEM(繫結的)和PTHREAD_SCOPE_PROCESS(非繫結的)。下面的代 *** 即建立了一個繫結的執行緒。
#include <pthread.h>
pthread_attr_t attr;
pthread_t tid;
/*初始化屬 *** 值,均設為預設值*/
pthread_attr_init(&attr);
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
pthread_create(&tid, &attr, (void *) my_function, NULL);
執行緒的分離狀態決定一個執行緒以什麼樣的方式來終止自己。在上面的例子中,我們採用了執行緒的預設屬 *** ,即為非分離狀態,這種情況下,原有的執行緒等待建立的執行緒結束。只有當pthread_join()函式返回時,建立的執行緒才算終止,才能釋放自己佔用的系統資源。而分離執行緒不是這樣子的,它沒有被其他的執行緒所等待,自己執行結束了,執行緒也就終止了,馬上釋放系統資源。程式設計師應該根據自己的需要,選擇適當的分離狀態。設定執行緒分離狀態的函式為pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)。第二個引數可選為PTHREAD_CREATE_DETACHED(分離執行緒)和 PTHREAD _CREATE_JOINABLE(非分離執行緒)。這裡要注意的一點是,如果設定一個執行緒為分離執行緒,而這個執行緒執行又非常快,它很可能在 pthread_create函式返回之前就終止了,它終止以後就可能將執行緒號和系統資源移交給其他的執行緒使用,這樣呼叫pthread_create的執行緒就得到了錯誤的執行緒號。要避免這種情況可以採取一定的同步措施,最簡單的方法之一是可以在被建立的執行緒裡呼叫 pthread_cond_timewait函式,讓這個執行緒等待一會兒,留出足夠的時間讓函式pthread_create返回。設定一段等待時間,是在多執行緒程式設計裡常用的方法。但是注意不要使用諸如wait()之類的函式,它們是使整個程式睡眠,並不能解決執行緒同步的問題。
另外一個可能常用的屬 *** 是執行緒的優先順序,它存放在結構sched_param中。用函式pthread_attr_getschedparam和函式 pthread_attr_setschedparam進行存放,一般說來,我們總是先取優先順序,對取得的值修改後再存放回去。下面即是一段簡單的例子。
#include <pthread.h>
#include <sched.h>
pthread_attr_t attr;
pthread_t tid;
sched_param param;
int newprio=20;
pthread_attr_init(&attr);
pthread_attr_getschedparam(&attr, ¶m);
param.sched_priority=newprio;
pthread_attr_setschedparam(&attr, ¶m);
pthread_create(&tid, &attr, (void *)myfunction, myarg);
4 執行緒的資料處理
和程式相比,執行緒的最大優點之一是資料的共享 *** ,各個程式共享父程式處沿襲的資料段,可以方便的獲得、修改資料。但這也給多執行緒程式設計帶來了許多問題。我們必須當心有多個不同的程式訪問相同的變數。許多函式是不可重入的,即同時不能執行一個函式的多個拷貝(除非使用不同的資料段)。在函式中宣告的靜態變數常常帶來問題,函式的返回值也會有問題。因為如果返回的是函式內部靜態宣告的空間的地址,則在一個執行緒呼叫該函式得到地址後使用該地址指向的資料時,別的執行緒可能呼叫此函式並修改了這一段資料。在程式中共享的變數必須用關鍵字volatile來定義,這是為了防止編譯器在優化時(如gcc中使用-OX引數)改變它們的使用方式。為了保護變數,我們必須使用訊號量、互斥等方法來保證我們對變數的正確使用。下面,我們就逐步介紹處理執行緒資料時的有關知識。
4.1 執行緒資料
在單執行緒的程式裡,有兩種基本的資料:全域性變數和區域性變數。但在多執行緒程式裡,還有第三種資料型別:執行緒資料(TSD: Thread-Specific Data)。它和全域性變數很象,線上程內部,各個函式可以象使用全域性變數一樣呼叫它,但它對執行緒外部的其它執行緒是不可見的。這種資料的必要 *** 是顯而易見的。例如我們常見的變數errno,它返回標準的出錯資訊。它顯然不能是一個區域性變數,幾乎每個函式都應該可以呼叫它;但它又不能是一個全域性變數,否則在A執行緒裡輸出的很可能是B執行緒的出錯資訊。要實現諸如此類的變數,我們就必須使用執行緒資料。我們為每個執行緒資料建立一個鍵,它和這個鍵相關聯,在各個執行緒裡,都使用這個鍵來指代執行緒資料,但在不同的執行緒裡,這個鍵代表的資料是不同的,在同一個執行緒裡,它代表同樣的資料內容。
和執行緒資料相關的函式主要有4個:建立一個鍵;為一個鍵指定執行緒資料;從一個鍵讀取執行緒資料;刪除鍵。
建立鍵的函式原型為:
extern int pthread_key_create __P ((pthread_key_t *__key,
void (*__destr_function) (void *)));
第一個引數為指向一個鍵值的指標,第二個引數指明瞭一個destructor函式,如果這個引數不為空,那麼當每個執行緒結束時,系統將呼叫這個函式來釋放繫結在這個鍵上的記憶體塊。這個函式常和函式pthread_once ((pthread_once_t*once_control, void (*initroutine) (void)))一起使用,為了讓這個鍵只被建立一次。函式pthread_once宣告一個初始化函式,第一次呼叫pthread_once時它執行這個函式,以後的呼叫將被它忽略。
在下面的例子中,我們建立一個鍵,並將它和某個資料相關聯。我們要定義一個函式 createWindow,這個函式定義一個圖形視窗(資料型別為Fl_Window *,這是圖形介面開發工具FLTK中的資料型別)。由於各個執行緒都會呼叫這個函式,所以我們使用執行緒資料。
/* 宣告一個鍵*/
pthread_key_t myWinKey;
/* 函式 createWindow */
void createWindow ( void ) {
Fl_Window * win;
static pthread_once_t>
Posix執行緒程式設計指南(1)
http://www-900.ibm.com/developer ... i/part5/index.shtml
內容:
一、 執行緒建立
二、執行緒取消
關於作者
執行緒建立與取消
楊沙洲(pubb@163.net)
2001 年 10 月
這是一個關於Posix執行緒程式設計的專欄。作者在闡明概念的基礎上,將向您詳細講述Posix執行緒庫API。本文是第一篇將向您講述執行緒的建立與取消。
一、執行緒建立
1.1 執行緒與程式
相對程式而言,執行緒是一個更加接近於執行體的概念,它可以與同程式中的其他執行緒共享資料,但擁有自己的棧空間,擁有獨立的執行序列。在序列程式基礎上引入執行緒和程式是為了提高程式的併發度,從而提高程式執行效率和響應時間。
執行緒和程式在使用上各有優缺點:執行緒執行開銷小,但不利於資源的管理和保護;而程式正相反。同時,執行緒適合於在SMP機器上執行,而程式則可以跨機器遷移。
1.2 建立執行緒
POSIX通過pthread_create()函式建立執行緒,API定義如下:
int pthread_create(pthread_t * thread, pthread_attr_t * attr,
void * (*start_routine)(void *), void * arg)
與 fork()呼叫建立一個程式的方法不同,pthread_create()建立的執行緒並不具備與主執行緒(即呼叫pthread_create()的執行緒)同樣的執行序列,而是使其執行start_routine(arg)函式。thread返回建立的執行緒ID,而attr是建立執行緒時設定的執行緒屬 *** (見下)。pthread_create()的返回值表示執行緒建立是否成功。儘管arg是void *型別的變數,但它同樣可以作為任意型別的引數傳給start_routine()函式;同時,start_routine()可以返回一個void *型別的返回值,而這個返回值也可以是其他型別,並由pthread_join()獲取。
1.3 執行緒建立屬 ***
pthread_create()中的attr引數是一個結構指標,結構中的元素分別對應著新執行緒的執行屬 *** ,主要包括以下幾項:
__detachstate,表示新執行緒是否與程式中其他執行緒脫離同步,如果置位則新執行緒不能用pthread_join()來同步,且在退出時自行釋放所佔用的資源。預設為 PTHREAD_CREATE_JOINABLE狀態。這個屬 *** 也可以線上程建立並執行以後用pthread_detach()來設定,而一旦設定為PTHREAD_CREATE_DETACH狀態(不論是建立時設定還是執行時設定)則不能再恢復到PTHREAD_CREATE_JOINABLE狀態。
__schedpolicy,表示新執行緒的排程策略,主要包括SCHED_OTHER(正常、非實時)、SCHED_RR(實時、輪轉法)和SCHED_FIFO(實時、先入先出)三種,預設為 SCHED_OTHER,後兩種排程策略僅對超級使用者有效。執行時可以用過pthread_setschedparam()來改變。
__schedparam,一個struct sched_param結構,目前僅有一個sched_priority整型變數表示執行緒的執行優先順序。這個引數僅當排程策略為實時(即SCHED_RR 或SCHED_FIFO)時才有效,並可以在執行時通過pthread_setschedparam()函式來改變,預設為0。
__inheritsched,有兩種值可供選擇:PTHREAD_EXPLICIT_SCHED和PTHREAD_INHERIT_SCHED,前者表示新執行緒使用顯式指定排程策略和排程引數(即attr中的值),而後者表示繼承呼叫者執行緒的值。預設為PTHREAD_EXPLICIT_SCHED。
__scope,表示執行緒間競爭CPU的範圍,也就是說執行緒優先順序的有效範圍。POSIX的標準中定義了兩個值:PTHREAD_SCOPE_SYSTEM和 PTHREAD_SCOPE_PROCESS,前者表示與系統中所有執行緒一起競爭CPU時間,後者表示僅與同程式中的執行緒競爭CPU。目前 LinuxThreads僅實現了PTHREAD_SCOPE_SYSTEM一值。
pthread_attr_t結構中還有一些值,但不使用pthread_create()來設定。
為了設定這些屬 *** ,POSIX定義了一系列屬 *** 設定函式,包括pthread_attr_init()、pthread_attr_destroy()和與各個屬 *** 相關的pthread_attr_get---/pthread_attr_set---函式。
1.4 執行緒建立的Linux實現
我們知道,Linux的執行緒實現是在核外進行的,核內提供的是建立程式的介面do_fork()。核心提供了兩個系統呼叫__clone()和 fork(),最終都用不同的引數呼叫do_fork()核內API。當然,要想實現執行緒,沒有核心對多程式(其實是輕量級程式)共享資料段的支援是不行的,因此,do_fork()提供了很多引數,包括CLONE_VM(共享記憶體空間)、CLONE_FS(共享檔案系統資訊)、 CLONE_FILES(共享檔案描述符表)、CLONE_SIGHAND(共享訊號控制程式碼表)和CLONE_PID(共享程式ID,僅對核內程式,即0號程式有效)。當使用fork系統呼叫時,核心呼叫do_fork()不使用任何共享屬 *** ,程式擁有獨立的執行環境,而使用pthread_create()來建立執行緒時,則最終設定了所有這些屬 *** 來呼叫__clone(),而這些引數又全部傳給核內的do_fork(),從而建立的"程式"擁有共享的執行環境,只有棧是獨立的,由 __clone()傳入。
Linux執行緒在核內是以輕量級程式的形式存在的,擁有獨立的程式表項,而所有的建立、同步、刪除等 *** 作都在核外pthread庫中進行。pthread庫使用一個管理執行緒(__pthread_manager(),每個程式獨立且唯一)來管理執行緒的建立和終止,為執行緒分配執行緒ID,傳送執行緒相關的訊號(比如Cancel),而主執行緒(pthread_create())的呼叫者則通過管道將請求資訊傳給管理執行緒。
二、執行緒取消
2.1 執行緒取消的定義
一般情況下,執行緒在其主體函式退出的時候會自動終止,但同時也可以因為接收到另一個執行緒發來的終止(取消)請求而強制終止。
2.2 執行緒取消的語義
執行緒取消的方法是向目標執行緒發Cancel訊號,但如何處理Cancel訊號則由目標執行緒自己決定,或者忽略、或者立即終止、或者繼續執行至Cancelation-point(取消點),由不同的Cancelation狀態決定。
執行緒接收到CANCEL訊號的預設處理(即pthread_create()建立執行緒的預設狀態)是繼續執行至取消點,也就是說設定一個CANCELED狀態,執行緒繼續執行,只有執行至Cancelation-point的時候才會退出。
2.3 取消點
根據POSIX標準,pthread_join()、pthread_testcancel()、pthread_cond_wait()、 pthread_cond_timedwait()、sem_wait()、sigwait()等函式以及read()、write()等會引起阻塞的系統呼叫都是Cancelation-point,而其他pthread函式都不會引起Cancelation動作。但是pthread_cancel的手冊頁聲稱,由於LinuxThread庫與C庫結合得不好,因而目前C庫函式都不是Cancelation-point;但CANCEL訊號會使執行緒從阻塞的系統呼叫中退出,並置EINTR錯誤 *** ,因此可以在需要作為Cancelation-point的系統呼叫前後呼叫pthread_testcancel(),從而達到POSIX標準所要求的目標,即如下代 *** 段:
pthread_testcancel();
retcode = read(fd, buffer, length);
pthread_testcancel();
2.4 程式設計方面的考慮
如果執行緒處於無限迴圈中,且迴圈體內沒有執行至取消點的必然路徑,則執行緒無法由外部其他執行緒的取消請求而終止。因此在這樣的迴圈體的必經路徑上應該加入pthread_testcancel()呼叫。
2.5 與執行緒取消相關的pthread函式
int pthread_cancel(pthread_t thread)
傳送終止訊號給thread執行緒,如果成功則返回0,否則為非0值。傳送成功並不意味著thread會終止。
int pthread_setcancelstate(int state, int *oldstate)
設定本執行緒對Cancel訊號的反應,state有兩種值:PTHREAD_CANCEL_ENABLE(預設)和 PTHREAD_CANCEL_DISABLE,分別表示收到訊號後設為CANCLED狀態和忽略CANCEL訊號繼續執行;old_state如果不為 NULL則存入原來的Cancel狀態以便恢復。
int pthread_setcanceltype(int type, int *oldtype)
設定本執行緒取消動作的執行時機,type由兩種取值:PTHREAD_CANCEL_DEFFERED和 PTHREAD_CANCEL_ASYCHRONOUS,僅當Cancel狀態為Enable時有效,分別表示收到訊號後繼續執行至下一個取消點再退出和立即執行取消動作(退出);oldtype如果不為NULL則存入運來的取消動作型別值。
void pthread_testcancel(void)
檢查本執行緒是否處於Canceld狀態,如果是,則進行取消動作,否則直接返回。
關於作者
楊沙洲,男,現攻讀國防科大計算機學院計算機軟體方向博士學位。您可以通過電子郵件pubb@163.net 跟他聯絡。
(c) Copyright IBM Corp. 2001, (c) Copyright IBM China 2001, All Right Reserved
關於 IBM | 隱私條約 | 使用條款 | 聯絡 IBM
-------------------------------------------------------------------------
Posix執行緒程式設計指南(2)
內容:
一. 概念及作用
二. 建立和登出
三. 訪問
四. 使用範例
關於作者
相關內容:
(1) 執行緒建立與取消
執行緒私有資料
楊沙洲(pubb@163.net)
2001 年 10 月
這是一個關於Posix執行緒程式設計的專欄。作者在闡明概念的基礎上,將向您詳細講述Posix執行緒庫API。本文是第二篇將向您講述執行緒的私有資料。
一.概念及作用
在單執行緒程式中,我們經常要用到"全域性變數"以實現多個函式間共享資料。在多執行緒環境下,由於資料空間是共享的,因此全域性變數也為所有執行緒所共有。但有時應用程式設計中有必要提供執行緒私有的全域性變數,僅在某個執行緒中有效,但卻可以跨多個函式訪問,比如程式可能需要每個執行緒維護一個連結串列,而使用相同的函式 *** 作,最簡單的辦法就是使用同名而不同變數地址的執行緒相關資料結構。這樣的資料結構可以由Posix執行緒庫維護,稱為執行緒私有資料(Thread- specific Data,或TSD)。
二.建立和登出
Posix定義了兩個API分別用來建立和登出TSD:
int pthread_key_create(pthread_key_t *key, void (*destr_function) (void *))
該函式從TSD池中分配一項,將其值賦給key供以後訪問使用。如果destr_function不為空,線上程退出(pthread_exit())時將以key所關聯的資料為引數呼叫destr_function(),以釋放分配的緩衝區。
不論哪個執行緒呼叫pthread_key_create(),所建立的key都是所有執行緒可訪問的,但各個執行緒可根據自己的需要往key中填入不同的值,這就相當於提供了一個同名而不同值的全域性變數。在LinuxThreads的實現中,TSD池用一個結構陣列表示:
static struct pthread_key_struct pthread_keys[PTHREAD_KEYS_MAX] = { { 0, NULL } };
建立一個TSD就相當於將結構陣列中的某一項設定為"in_use",並將其索引返回給*key,然後設定destructor函式為destr_function。
登出一個TSD採用如下API:
int pthread_key_delete(pthread_key_t key)
這個函式並不檢查當前是否有執行緒正使用該TSD,也不會呼叫清理函式(destr_function),而只是將TSD釋放以供下一次呼叫 pthread_key_create()使用。在LinuxThreads中,它還會將與之相關的執行緒資料項設為NULL(見"訪問")。
三.訪問
TSD的讀寫都通過專門的Posix Thread函式進行,其API定義如下:
int pthread_setspecific(pthread_key_t key, const void *pointer)
void * pthread_getspecific(pthread_key_t key)
寫入(pthread_setspecific())時,將pointer的值(不是所指的內容)與key相關聯,而相應的讀出函式則將與key相關聯的資料讀出來。資料型別都設為void *,因此可以指向任何型別的資料。
在LinuxThreads中,使用了一個位於執行緒描述結構(_pthread_descr_struct)中的二維void *指標陣列來存放與key關聯的資料,陣列大小由以下幾個巨集來說明:
#define PTHREAD_KEY_2NDLEVEL_SIZE 32
#define PTHREAD_KEY_1STLEVEL_SIZE \
((PTHREAD_KEYS_MAX + PTHREAD_KEY_2NDLEVEL_SIZE - 1)
/ PTHREAD_KEY_2NDLEVEL_SIZE)
其中在/usr/include/bits/local_lim.h中定義了PTHREAD_KEYS_MAX為1024,因此一維陣列大小為32。而具體存放的位置由key值經過以下計算得到:
idx1st = key / PTHREAD_KEY_2NDLEVEL_SIZE
idx2nd = key % PTHREAD_KEY_2NDLEVEL_SIZE
也就是說,資料存放與一個32×32的稀疏矩陣中。同樣,訪問的時候也由key值經過類似計算得到資料所在位置索引,再取出其中內容返回。
四.使用範例
以下這個例子沒有什麼實際意義,只是說明如何使用,以及能夠使用這一機制達到儲存執行緒私有資料的目的。
#include <stdio.h>
#include <pthread.h>
pthread_key_t key;
void echomsg(int t)
{
printf("destructor excuted in thread %d,param=%d\n",pthread_self(),t);
}
void * child1(void *arg)
{
int tid=pthread_self();
printf("thread %d enter\n",tid);
pthread_setspecific(key,(void *)tid);
sleep(2);
printf("thread %d returns %d\n",tid,pthread_getspecific(key));
sleep(5);
}
void * child2(void *arg)
{
int tid=pthread_self();
printf("thread %d enter\n",tid);
pthread_setspecific(key,(void *)tid);
sleep(1);
printf("thread %d returns %d\n",tid,pthread_getspecific(key));
sleep(5);
}
int main(void)
{
int tid1,tid2;
printf("hello\n");
pthread_key_create(&key,echomsg);
pthread_create(&tid1,NULL,child1,NULL);
pthread_create(&tid2,NULL,child2,NULL);
sleep(10);
pthread_key_delete(key);
printf("main thread exit\n");
return 0;
}
給例程建立兩個執行緒分別設定同一個執行緒私有資料為自己的執行緒ID,為了檢驗其私有 *** ,程式錯開了兩個執行緒私有資料的寫入和讀出的時間,從程式執行結果可以看出,兩個執行緒對TSD的修改互不干擾。同時,當執行緒退出時,清理函式會自動執行,引數為tid。
Posix執行緒程式設計指南(3)
內容:
一. 互斥鎖
二. 條件變數
三. 訊號燈
四. 非同步訊號
五. 其他同步方式
關於作者
相關內容:
(1) 執行緒建立與取消
(2) 執行緒私有資料
執行緒同步
楊沙洲(pubb@163.net)
2001 年 10 月
這是一個關於Posix執行緒程式設計的專欄。作者在闡明概念的基礎上,將向您詳細講述Posix執行緒庫API。本文是第三篇,將向您講述執行緒同步。
一.互斥鎖
儘管在Posix Thread中同樣可以使用IPC的訊號量機制來實現互斥鎖mutex功能,但顯然semphore的功能過於強大了,在Posix Thread中定義了另外一套專門用於執行緒同步的mutex函式。
1. 建立和銷燬
有兩種方法建立互斥鎖,靜態方式和動態方式。POSIX定義了一個巨集PTHREAD_MUTEX_INITIALIZER來靜態初始化互斥鎖,方法如下:
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
在LinuxThreads實現中,pthread_mutex_t是一個結構,而PTHREAD_MUTEX_INITIALIZER則是一個結構常量。
動態方式是採用pthread_mutex_init()函式來初始化互斥鎖,API定義如下:
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
其中mutexattr用於指定互斥鎖屬 *** (見下),如果為NULL則使用預設屬 *** 。
pthread_mutex_destroy()用於登出一個互斥鎖,API定義如下:
int pthread_mutex_destroy(pthread_mutex_t *mutex)
銷燬一個互斥鎖即意味著釋放它所佔用的資源,且要求鎖當前處於開放狀態。由於在Linux中,互斥鎖並不佔用任何資源,因此LinuxThreads中的pthread_mutex_destroy()除了檢查鎖狀態以外(鎖定狀態則返回EBUSY)沒有其他動作。
2. 互斥鎖屬 ***
互斥鎖的屬 *** 在建立鎖的時候指定,在LinuxThreads實現中僅有一個鎖型別屬 *** ,不同的鎖型別在試圖對一個已經被鎖定的互斥鎖加鎖時表現不同。當前(glibc2.2.3,linuxthreads0.9)有四個值可供選擇:
PTHREAD_MUTEX_TIMED_NP,這是預設值,也就是普通鎖。當一個執行緒加鎖以後,其餘請求鎖的執行緒將形成一個等待佇列,並在解鎖後按優先順序獲得鎖。這種鎖策略保證了資源分配的公平 *** 。
PTHREAD_MUTEX_RECURSIVE_NP,巢狀鎖,允許同一個執行緒對同一個鎖成功獲得多次,並通過多次unlock解鎖。如果是不同執行緒請求,則在加鎖執行緒解鎖時重新競爭。
PTHREAD_MUTEX_ERRORCHECK_NP,檢錯鎖,如果同一個執行緒請求同一個鎖,則返回EDEADLK,否則與PTHREAD_MUTEX_TIMED_NP型別動作相同。這樣就保證當不允許多次加鎖時不會出現最簡單情況下的死鎖。
PTHREAD_MUTEX_ADAPTIVE_NP,適應鎖,動作最簡單的鎖型別,僅等待解鎖後重新競爭。
3. 鎖 *** 作
鎖 *** 作主要包括加鎖pthread_mutex_lock()、解鎖pthread_mutex_unlock()和測試加鎖 pthread_mutex_trylock()三個,不論哪種型別的鎖,都不可能被兩個不同的執行緒同時得到,而必須等待解鎖。對於普通鎖和適應鎖型別,解鎖者可以是同程式內任何執行緒;而檢錯鎖則必須由加鎖者解鎖才有效,否則返回EPERM;對於巢狀鎖,文件和實現要求必須由加鎖者解鎖,但實驗結果表明並沒有這種 *** ,這個不同目前還沒有得到解釋。在同一程式中的執行緒,如果加鎖後沒有解鎖,則任何其他執行緒都無法再獲得鎖。
int pthread_mutex_lock(pthread_mutex_t *mutex)
int pthread_mutex_unlock(pthread_mutex_t *mutex)
int pthread_mutex_trylock(pthread_mutex_t *mutex)
pthread_mutex_trylock()語義與pthread_mutex_lock()類似,不同的是在鎖已經被佔據時返回EBUSY而不是掛起等待。
4. 其他
POSIX 執行緒鎖機制的Linux實現都不是取消點,因此,延遲取消型別的執行緒不會因收到取消訊號而離開加鎖等待。值得注意的是,如果執行緒在加鎖後解鎖前被取消,鎖將永遠保持鎖定狀態,因此如果在關鍵區段內有取消點存在,或者設定了非同步取消型別,則必須在退出回撥函式中解鎖。
這個鎖機制同時也不是非同步訊號安全的,也就是說,不應該在訊號處理過程中使用互斥鎖,否則容易造成死鎖。
二.條件變數
條件變數是利用執行緒間共享的全域性變數進行同步的一種機制,主要包括兩個動作:一個執行緒等待"條件變數的條件成立"而掛起;另一個執行緒使"條件成立"(給出條件成立訊號)。為了防止競爭,條件變數的使用總是和一個互斥鎖結合在一起。
1. 建立和登出
條件變數和互斥鎖一樣,都有靜態動態兩種建立方式,靜態方式使用PTHREAD_COND_INITIALIZER常量,如下:
pthread_cond_t cond=PTHREAD_COND_INITIALIZER
動態方式呼叫pthread_cond_init()函式,API定義如下:
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr)
儘管POSIX標準中為條件變數定義了屬 *** ,但在LinuxThreads中沒有實現,因此cond_attr值通常為NULL,且被忽略。
登出一個條件變數需要呼叫pthread_cond_destroy(),只有在沒有執行緒在該條件變數上等待的時候才能登出這個條件變數,否則返回EBUSY。因為Linux實現的條件變數沒有分配什麼資源,所以登出動作只包括檢查是否有等待執行緒。API定義如下:
int pthread_cond_destroy(pthread_cond_t *cond)
2. 等待和激發
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime)
等待條件有兩種方式:無條件等待pthread_cond_wait()和計時等待pthread_cond_timedwait(),其中計時等待方式如果在給定時刻前條件沒有滿足,則返回ETIMEOUT,結束等待,其中abstime以與time()系統呼叫相同意義的絕對時間形式出現,0表示格林尼治時間1970年1月1日0時0分0秒。
無論哪種等待方式,都必須和一個互斥鎖配合,以防止多個執行緒同時請求pthread_cond_wait()(或 pthread_cond_timedwait(),下同)的競爭條件(Race Condition)。mutex互斥鎖必須是普通鎖(PTHREAD_MUTEX_TIMED_NP)或者適應鎖(PTHREAD_MUTEX_ADAPTIVE_NP),且在呼叫pthread_cond_wait()前必須由本執行緒加鎖(pthread_mutex_lock()),而在更新條件等待佇列以前,mutex保持鎖定狀態,並線上程掛起進入等待前解鎖。在條件滿足從而離開 pthread_cond_wait()之前,mutex將被重新加鎖,以與進入pthread_cond_wait()前的加鎖動作對應。
激發條件有兩種形式,pthread_cond_signal()啟用一個等待該條件的執行緒,存在多個等待執行緒時按入隊順序啟用其中一個;而pthread_cond_broadcast()則啟用所有等待執行緒。
3. 其他
pthread_cond_wait() 和pthread_cond_timedwait()都被實現為取消點,因此,在該處等待的執行緒將立即重新執行,在重新鎖定mutex後離開 pthread_cond_wait(),然後執行取消動作。也就是說如果pthread_cond_wait()被取消,mutex是保持鎖定狀態的,因而需要定義退出回撥函式來為其解鎖。
以下示例集中演示了互斥鎖和條件變數的結合使用,以及取消對於條件等待動作的影響。在例子中,有兩個執行緒被啟動,並等待同一個條件變數,如果不使用退出回撥函式(見範例中的註釋部分),則tid2將在pthread_mutex_lock()處永久等待。如果使用回撥函式,則tid2的條件等待及主執行緒的條件激發都能正常工作。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex;
pthread_cond_t cond;
void * child1(void *arg)
{
pthread_cleanup_push(pthread_mutex_unlock,&mutex); /* comment 1 */
while(1){
printf("thread 1 get running \n");
printf("thread 1 pthread_mutex_lock returns %d\n",
pthread_mutex_lock(&mutex));
pthread_cond_wait(&cond,&mutex);
printf("thread 1 condition applied\n");
pthread_mutex_unlock(&mutex);
sleep(5);
}
pthread_cleanup_pop(0); /* comment 2 */
}
void *child2(void *arg)
{
while(1){
sleep(3); /* comment 3 */
printf("thread 2 get running.\n");
printf("thread 2 pthread_mutex_lock returns %d\n",
pthread_mutex_lock(&mutex));
pthread_cond_wait(&cond,&mutex);
printf("thread 2 condition applied\n");
pthread_mutex_unlock(&mutex);
sleep(1);
}
}
int main(void)
{
int tid1,tid2;
printf("hello, condition variable test\n");
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&cond,NULL);
pthread_create(&tid1,NULL,child1,NULL);
pthread_create(&tid2,NULL,child2,NULL);
do{
sleep(2); /* comment 4 */
pthread_cancel(tid1); /* comment 5 */
sleep(2); /* comment 6 */
pthread_cond_signal(&cond);
}while(1);
sleep(100);
pthread_exit(0);
}
如果不做註釋5的pthread_cancel()動作,即使沒有那些sleep()延時 *** 作,child1和child2都能正常工作。註釋3和註釋4的延遲使得child1有時間完成取消動作,從而使child2能在child1退出之後進入請求鎖 *** 作。如果沒有註釋1和註釋2的回撥函式定義,系統將掛起在child2請求鎖的地方;而如果同時也不做註釋3和註釋4的延時,child2能在 child1完成取消動作以前得到控制,從而順利執行申請鎖的 *** 作,但卻可能掛起在pthread_cond_wait()中,因為其中也有申請mutex的 *** 作。child1函式給出的是標準的條件變數的使用方式:回撥函式保護,等待條件前鎖定,pthread_cond_wait()返回後解鎖。
條件變數機制不是非同步訊號安全的,也就是說,在訊號處理函式中呼叫pthread_cond_signal()或者pthread_cond_broadcast()很可能引起死鎖。
三.訊號燈
訊號燈與互斥鎖和條件變數的主要不同在於"燈"的概念,燈亮則意味著資源可用,燈滅則意味著不可用。如果說後兩中同步方式側重於"等待" *** 作,即資源不可用的話,訊號燈機制則側重於點燈,即告知資源可用;沒有等待執行緒的解鎖或激發條件都是沒有意義的,而沒有等待燈亮的執行緒的點燈 *** 作則有效,且能保持燈亮狀態。當然,這樣的 *** 作原語也意味著更多的開銷。
訊號燈的應用除了燈亮/燈滅這種二元燈以外,也可以採用大於1的燈數,以表示資源數大於1,這時可以稱之為多元燈。
1. 建立和登出
POSIX訊號燈標準定義了有名訊號燈和無名訊號燈兩種,但LinuxThreads的實現僅有無名燈,同時有名燈除了總是可用於多程式之間以外,在使用上與無名燈並沒有很大的區別,因此下面僅就無名燈進行討論。
int sem_init(sem_t *sem, int pshared, unsigned int value)
這是建立訊號燈的API,其中value為訊號燈的初值,pshared表示是否為多程式共享而不僅僅是用於一個程式。LinuxThreads沒有實現多程式共享訊號燈,因此所有非0值的pshared輸入都將使sem_init()返回-1,且置errno為ENOSYS。初始化好的訊號燈由sem變數表徵,用於以下點燈、滅燈 *** 作。
int sem_destroy(sem_t * sem)
被登出的訊號燈sem要求已沒有執行緒在等待該訊號燈,否則返回-1,且置errno為EBUSY。除此之外,LinuxThreads的訊號燈登出函式不做其他動作。
2. 點燈和滅燈
int sem_post(sem_t * sem)
點燈 *** 作將訊號燈值原子地加1,表示增加一個可訪問的資源。
int sem_wait(sem_t * sem)
int sem_trywait(sem_t * sem)
sem_wait()為等待燈亮 *** 作,等待燈亮(訊號燈值大於0),然後將訊號燈原子地減1,並返回。sem_trywait()為sem_wait()的非阻塞版,如果訊號燈計數大於0,則原子地減1並返回0,否則立即返回-1,errno置為EAGAIN。
3. 獲取燈值
int sem_getvalue(sem_t * sem, int * sval)
讀取sem中的燈計數,存於*sval中,並返回0。
4. 其他
sem_wait()被實現為取消點,而且在支援原子"比較且交換"指令的體系結構上,sem_post()是唯一能用於非同步訊號處理函式的POSIX非同步訊號安全的API。
四.非同步訊號
由於LinuxThreads是在核外使用核內輕量級程式實現的執行緒,所以基於核心的非同步訊號 *** 作對於執行緒也是有效的。但同時,由於非同步訊號總是實際發往某個程式,所以無法實現POSIX標準所要求的"訊號到達某個程式,然後再由該程式將訊號分發到所有沒有阻塞該訊號的執行緒中"原語,而是隻能影響到其中一個執行緒。
POSIX非同步訊號同時也是一個標準C庫提供的功能,主要包括訊號集管理(sigemptyset()、sigfillset()、sigaddset()、sigdelset()、sigismember()等)、訊號處理函式安裝(sigaction())、訊號阻塞控制(sigprocmask())、被阻塞訊號查詢(sigpending())、訊號等待 (sigsuspend())等,它們與傳送訊號的kill()等函式配合就能實現程式間非同步訊號功能。LinuxThreads圍繞執行緒封裝了 sigaction()和raise(),本節集中討論LinuxThreads中擴充套件的非同步訊號函式,包括pthread_sigmask()、 pthread_kill()和sigwait()三個函式。毫無疑問,所有POSIX非同步訊號函式對於執行緒都是可用的。
int pthread_sigmask(int how, const sigset_t *newmask, sigset_t *oldmask)
設定執行緒的訊號遮蔽 *** ,語義與sigprocmask()相同,但對不允許遮蔽的Cancel訊號和不允許響應的Restart訊號進行了保護。被遮蔽的訊號儲存在訊號佇列中,可由sigpending()函式取出。
int pthread_kill(pthread_t thread, int signo)
向thread號執行緒傳送signo訊號。實現中在通過thread執行緒號定位到對應程式號以後使用kill()系統呼叫完成傳送。
int sigwait(const sigset_t *set, int *sig)
掛起執行緒,等待set中指定的訊號之一到達,並將到達的訊號存入*sig中。POSIX標準建議在呼叫sigwait()等待訊號以前,程式中所有執行緒都應遮蔽該訊號,以保證僅有sigwait()的呼叫者獲得該訊號,因此,對於需要等待同步的非同步訊號,總是應該在建立任何執行緒以前呼叫 pthread_sigmask()遮蔽該訊號的處理。而且,呼叫sigwait()期間,原來附接在該訊號上的訊號處理函式不會被呼叫。
如果在等待期間接收到Cancel訊號,則立即退出等待,也就是說sigwait()被實現為取消點。
五.其他同步方式
除了上述討論的同步方式以外,其他很多程式間通訊手段對於LinuxThreads也是可用的,比如基於檔案系統的IPC(管道、Unix域Socket 等)、訊息佇列(Sys.V或者Posix的)、System V的訊號燈等。只有一點需要注意,LinuxThreads在核內是作為共享儲存區、共享檔案系統屬 *** 、共享訊號處理、共享檔案描述符的獨立程式看待的。
Posix執行緒程式設計指南(4)
內容:
1. 執行緒終止方式
2. 執行緒終止時的清理
3. 執行緒終止的同步及其返回值
4. 關於pthread_exit()和return
參考資料
關於作者
相關內容:
(1) 執行緒建立與取消
(2) 執行緒私有資料
(3) 執行緒同步
執行緒終止
楊沙洲(pubb@163.net)
2001 年 11 月
這是一個關於Posix執行緒程式設計的專欄。作者在闡明概念的基礎上,將向您詳細講述Posix執行緒庫API。本文是第四篇將向您講述執行緒中止。
1.執行緒終止方式
一般來說,Posix的執行緒終止有兩種情況:正常終止和非正常終止。執行緒主動呼叫pthread_exit()或者從執行緒函式中return都將使執行緒正常退出,這是可預見的退出方式;非正常終止是執行緒在其他執行緒的干預下,或者由於自身執行出錯(比如訪問非法地址)而退出,這種退出方式是不可預見的。
2.執行緒終止時的清理
不論是可預見的執行緒終止還是異常終止,都會存在資源釋放的問題,在不考慮因執行出錯而退出的前提下,如何保證執行緒終止時能順利的釋放掉自己所佔用的資源,特別是鎖資源,就是一個必須考慮解決的問題。
最經常出現的情形是資源獨佔鎖的使用:執行緒為了訪問臨界資源而為其加上鎖,但在訪問過程中被外界取消,如果執行緒處於響應取消狀態,且採用非同步方式響應,或者在開啟獨佔鎖以前的執行路徑上存在取消點,則該臨界資源將永遠處於鎖定狀態得不到釋放。外界取消 *** 作是不可預見的,因此的確需要一個機制來簡化用於資源釋放的程式設計。
在POSIX執行緒API中提供了一個 pthread_cleanup_push()/pthread_cleanup_pop()函式對用於自動釋放資源--從 pthread_cleanup_push()的呼叫點到pthread_cleanup_pop()之間的程式段中的終止動作(包括呼叫 pthread_exit()和取消點終止)都將執行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_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);
/* do some work */
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);
}
3.執行緒終止的同步及其返回值
一般情況下,程式中各個執行緒的執行都是相互獨立的,執行緒的終止並不會通知,也不會影響其他執行緒,終止的執行緒所佔用的資源也並不會隨著執行緒的終止而得到釋放。正如程式之間可以用wait()系統呼叫來同步終止並釋放資源一樣,執行緒之間也有類似機制,那就是pthread_join()函式。
void pthread_exit(void *retval)
int pthread_join(pthread_t th, void **thread_return)
int pthread_detach(pthread_t th)
pthread_join() 的呼叫者將掛起並等待th執行緒終止,retval是pthread_exit()呼叫者執行緒(執行緒ID為th)的返回值,如果thread_return 不為NULL,則*thread_return=retval。需要注意的是一個執行緒僅允許唯一的一個執行緒使用pthread_join()等待它的終止,並且被等待的執行緒應該處於可join狀態,即非DETACHED狀態。
如果程式中的某個執行緒執行了 pthread_detach(th),則th執行緒將處於DETACHED狀態,這使得th執行緒在結束執行時自行釋放所佔用的記憶體資源,同時也無法由 pthread_join()同步,pthread_detach()執行之後,對th請求pthread_join()將返回錯誤。
一個可join的執行緒所佔用的記憶體僅當有執行緒對其執行了pthread_join()後才會釋放,因此為了避免記憶體洩漏,所有執行緒的終止,要麼已設為DETACHED,要麼就需要使用pthread_join()來回收。
4.關於pthread_exit()和return
理論上說,pthread_exit()和執行緒宿體函式退出的功能是相同的,函式結束時會在內部自動呼叫pthread_exit()來清理執行緒相關的資源。但實際上二者由於編譯器的處理有很大的不同。
在程式主函式(main())中呼叫pthread_exit(),只會使主函式所在的執行緒(可以說是程式的主執行緒)退出;而如果是return,編譯器將使其呼叫程式退出的代 *** (如_exit()),從而導致程式及其所有執行緒結束執行。
其次,線上程宿主函式中主動呼叫return,如果return語句包含在pthread_cleanup_push()/pthread_cleanup_pop()對中,則不會引起清理函式的執行,反而會導致segment fault。
Posix執行緒程式設計指南(5)
內容:
1.獲得本執行緒ID
2.判斷兩個執行緒是否為同一執行緒
3.僅執行一次的 *** 作
4.pthread_kill_other_threads_np()
關於作者
相關內容:
(1) 執行緒建立與取消
(2) 執行緒私有資料
(3) 執行緒同步
(4) 執行緒終止
雜項
楊沙洲(pubb@163.net)
2001 年 11 月
這是一個關於Posix執行緒程式設計的專欄。作者在闡明概念的基礎上,將向您詳細講述Posix執行緒庫API。本文是第五篇將向您講述pthread_self()、pthread_equal()和pthread_once()等雜項函式。
在 Posix執行緒規範中還有幾個輔助函式難以歸類,暫且稱其為雜項函式,主要包括pthread_self()、pthread_equal()和 pthread_once()三個,另外還有一個LinuxThreads非可移植 *** 擴充套件函式pthread_kill_other_threads_np()。本文就介紹這幾個函式的定義和使用。
1.獲得本執行緒ID
pthread_t pthread_self(void)
本函式返回本執行緒的識別符號。
在LinuxThreads中,每個執行緒都用一個pthread_descr結構來描述,其中包含了執行緒狀態、執行緒ID等所有需要的資料結構,此函式的實現就是線上程棧幀中找到本執行緒的pthread_descr結構,然後返回其中的p_tid項。
pthread_t型別在LinuxThreads中定義為無符號長整型。
2.判斷兩個執行緒是否為同一執行緒
int pthread_equal(pthread_t thread1, pthread_t thread2)
判斷兩個執行緒描述符是否指向同一執行緒。在LinuxThreads中,執行緒ID相同的執行緒必然是同一個執行緒,因此,這個函式的實現僅僅判斷thread1和thread2是否相等。
3.僅執行一次的 *** 作
int pthread_once(pthread_once_t *once_control, void (*init_routine) (void))
本函式使用初值為PTHREAD_ONCE_INIT的once_control變數保證init_routine()函式在本程式執行序列中僅執行一次。
#include <stdio.h>
#include <pthread.h>
pthread_once_t once=PTHREAD_ONCE_INIT;
void >
作者: 姚繼鋒 (2001-08-11 09:05:00)
1 引言
執行緒(thread)技術早在60年代就被提出,但真正應用多執行緒到 *** 作系統中去,是在80年代中期,solaris是這方面的佼佼者。傳統的Unix也支援執行緒的概念,但是在一個程式(process)中只允許有一個執行緒,這樣多執行緒就意味著多程式。現在,多執行緒技術已經被許多 *** 作系統所支援,包括Windows/NT,當然,也包括Linux。
為什麼有了程式的概念後,還要再引入執行緒呢?使用多執行緒到底有哪些好處?什麼的系統應該選用多執行緒?我們首先必須回答這些問題。
使用多執行緒的理由之一是和程式相比,它是一種非常"節儉"的多工 *** 作方式。我們知道,在Linux系統下,啟動一個新的程式必須分配給它獨立的地址空間,建立眾多的資料表來維護它的代 *** 段、堆疊段和資料段,這是一種"昂貴"的多工工作方式。而執行於一個程式中的多個執行緒,它們彼此之間使用相同的地址空間,共享大部分資料,啟動一個執行緒所花費的空間遠遠小於啟動一個程式所花費的空間,而且,執行緒間彼此切換所需的時間也遠遠小於程式間切換所需要的時間。據統計,總的說來,一個程式的開銷大約是一個執行緒開銷的30倍左右,當然,在具體的系統上,這個資料可能會有較大的區別。
使用多執行緒的理由之二是執行緒間方便的通訊機制。對不同程式來說,它們具有獨立的資料空間,要進行資料的傳遞只能通過通訊的方式進行,這種方式不僅費時,而且很不方便。執行緒則不然,由於同一程式下的執行緒之間共享資料空間,所以一個執行緒的資料可以直接為其它執行緒所用,這不僅快捷,而且方便。當然,資料的共享也帶來其他一些問題,有的變數不能同時被兩個執行緒所修改,有的子程式中宣告為static的資料更有可能給多執行緒程式帶來災難 *** 的打擊,這些正是編寫多執行緒程式時最需要注意的地方。
除了以上所說的優點外,不和程式比較,多執行緒程式作為一種多工、併發的工作方式,當然有以下的優點:
1) 提高應用程式響應。這對圖形介面的程式尤其有意義,當一個 *** 作耗時很長時,整個系統都會等待這個 *** 作,此時程式不會響應鍵盤、滑鼠、選單的 *** 作,而使用多執行緒技術,將耗時長的 *** 作(time consuming)置於一個新的執行緒,可以避免這種尷尬的情況。
2) 使多CPU系統更加有效。 *** 作系統會保證當執行緒數不大於CPU數目時,不同的執行緒執行於不同的CPU上。
3) 改善程式結構。一個既長又複雜的程式可以考慮分為多個執行緒,成為幾個獨立或半獨立的執行部分,這樣的程式會利於理解和修改。
下面我們先來嘗試編寫一個簡單的多執行緒程式。
2 簡單的多執行緒程式設計
Linux系統下的多執行緒遵循POSIX執行緒介面,稱為pthread。編寫Linux下的多執行緒程式,需要使用標頭檔案pthread.h,連線時需要使用庫libpthread.a。順便說一下,Linux下pthread的實現是通過系統呼叫clone()來實現的。clone()是Linux所特有的系統呼叫,它的使用方式類似fork,關於clone()的詳細情況,有興趣的讀者可以去檢視有關文件說明。下面我們展示一個最簡單的多執行緒程式 example1.c。
/* example.c*/
#include <stdio.h>
#include <pthread.h>
void thread(void)
{
int i;
for(i=0;i<3;i++)
printf("This is a pthread.\n");
}
int main(void)
{
pthread_t id;
int i,ret;
ret=pthread_create(&id,NULL,(void *) thread,NULL);
if(ret!=0){
printf ("Create pthread error!\n");
exit (1);
}
for(i=0;i<3;i++)
printf("This is the main process.\n");
pthread_join(id,NULL);
return (0);
}
我們編譯此程式:
gcc example1.c -lpthread -o example1
執行example1,我們得到如下結果:
This is the main process.
This is a pthread.
This is the main process.
This is the main process.
This is a pthread.
This is a pthread.
再次執行,我們可能得到如下結果:
This is a pthread.
This is the main process.
This is a pthread.
This is the main process.
This is a pthread.
This is the main process.
前後兩次結果不一樣,這是兩個執行緒爭奪CPU資源的結果。上面的示例中,我們使用到了兩個函式, pthread_create和pthread_join,並宣告瞭一個pthread_t型的變數。
pthread_t在標頭檔案/usr/include/bits/pthreadtypes.h中定義:
typedef unsigned long int pthread_t;
它是一個執行緒的識別符號。函式pthread_create用來建立一個執行緒,它的原型為:
extern int pthread_create __P ((pthread_t *__thread, __const pthread_attr_t *__attr,
void *(*__start_routine) (void *), void *__arg));
第一個引數為指向執行緒識別符號的指標,第二個引數用來設定執行緒屬 *** ,第三個引數是執行緒執行函式的起始地址,最後一個引數是執行函式的引數。這裡,我們的函式thread不需要引數,所以最後一個引數設為空指標。第二個引數我們也設為空指標,這樣將生成預設屬 *** 的執行緒。對執行緒屬 *** 的設定和修改我們將在下一節闡述。當建立執行緒成功時,函式返回0,若不為0則說明建立執行緒失敗,常見的錯誤返回代 *** 為EAGAIN和EINVAL。前者表示系統 *** 建立新的執行緒,例如執行緒數目過多了;後者表示第二個引數代表的執行緒屬 *** 值非法。建立執行緒成功後,新建立的執行緒則執行引數三和引數四確定的函式,原來的執行緒則繼續執行下一行代 *** 。
函式pthread_join用來等待一個執行緒的結束。函式原型為:
extern int pthread_join __P ((pthread_t __th, void **__thread_return));
第一個引數為被等待的執行緒識別符號,第二個引數為一個使用者定義的指標,它可以用來儲存被等待執行緒的返回值。這個函式是一個執行緒阻塞的函式,呼叫它的函式將一直等待到被等待的執行緒結束為止,當函式返回時,被等待執行緒的資源被收回。一個執行緒的結束有兩種途徑,一種是象我們上面的例子一樣,函式結束了,呼叫它的執行緒也就結束了;另一種方式是通過函式pthread_exit來實現。它的函式原型為:
extern void pthread_exit __P ((void *__retval)) __attribute__ ((__noreturn__));
唯一的引數是函式的返回代 *** ,只要pthread_join中的第二個引數thread_return不是NULL,這個值將被傳遞給thread_return。最後要說明的是,一個執行緒不能被多個執行緒等待,否則第一個接收到訊號的執行緒成功返回,其餘呼叫pthread_join的執行緒則返回錯誤代 *** ESRCH。
在這一節裡,我們編寫了一個最簡單的執行緒,並掌握了最常用的三個函式pthread_create,pthread_join和pthread_exit。下面,我們來瞭解執行緒的一些常用屬 *** 以及如何設定這些屬 *** 。
3 修改執行緒的屬 ***
在上一節的例子裡,我們用pthread_create函式建立了一個執行緒,在這個執行緒中,我們使用了預設引數,即將該函式的第二個引數設為NULL。的確,對大多數程式來說,使用預設屬 *** 就夠了,但我們還是有必要來了解一下執行緒的有關屬 *** 。
屬 *** 結構為pthread_attr_t,它同樣在標頭檔案/usr/include/pthread.h中定義,喜歡追根問底的人可以自己去檢視。屬 *** 值不能直接設定,須使用相關函式進行 *** 作,初始化的函式為pthread_attr_init,這個函式必須在pthread_create函式之前呼叫。屬 *** 物件主要包括是否繫結、是否分離、堆疊地址、堆疊大小、優先順序。預設的屬 *** 為非繫結、非分離、預設1M的堆疊、與父程式同樣級別的優先順序。
關於執行緒的繫結,牽涉到另外一個概念:輕程式(LWP:Light Weight Process)。輕程式可以理解為核心執行緒,它位於使用者層和系統層之間。系統對執行緒資源的分配、對執行緒的控制是通過輕程式來實現的,一個輕程式可以控制一個或多個執行緒。預設狀況下,啟動多少輕程式、哪些輕程式來控制哪些執行緒是由系統來控制的,這種狀況即稱為非繫結的。繫結狀況下,則顧名思義,即某個執行緒固定的"綁"在一個輕程式之上。被繫結的執行緒具有較高的響應速度,這是因為CPU時間片的排程是面向輕程式的,繫結的執行緒可以保證在需要的時候它總有一個輕程式可用。通過設定被繫結的輕程式的優先順序和排程級可以使得繫結的執行緒滿足諸如實時反應之類的要求。
設定執行緒繫結狀態的函式為pthread_attr_setscope,它有兩個引數,第一個是指向屬 *** 結構的指標,第二個是繫結型別,它有兩個取值:PTHREAD_SCOPE_SYSTEM(繫結的)和PTHREAD_SCOPE_PROCESS(非繫結的)。下面的代 *** 即建立了一個繫結的執行緒。
#include <pthread.h>
pthread_attr_t attr;
pthread_t tid;
/*初始化屬 *** 值,均設為預設值*/
pthread_attr_init(&attr);
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
pthread_create(&tid, &attr, (void *) my_function, NULL);
執行緒的分離狀態決定一個執行緒以什麼樣的方式來終止自己。在上面的例子中,我們採用了執行緒的預設屬 *** ,即為非分離狀態,這種情況下,原有的執行緒等待建立的執行緒結束。只有當pthread_join()函式返回時,建立的執行緒才算終止,才能釋放自己佔用的系統資源。而分離執行緒不是這樣子的,它沒有被其他的執行緒所等待,自己執行結束了,執行緒也就終止了,馬上釋放系統資源。程式設計師應該根據自己的需要,選擇適當的分離狀態。設定執行緒分離狀態的函式為pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)。第二個引數可選為PTHREAD_CREATE_DETACHED(分離執行緒)和 PTHREAD _CREATE_JOINABLE(非分離執行緒)。這裡要注意的一點是,如果設定一個執行緒為分離執行緒,而這個執行緒執行又非常快,它很可能在 pthread_create函式返回之前就終止了,它終止以後就可能將執行緒號和系統資源移交給其他的執行緒使用,這樣呼叫pthread_create的執行緒就得到了錯誤的執行緒號。要避免這種情況可以採取一定的同步措施,最簡單的方法之一是可以在被建立的執行緒裡呼叫 pthread_cond_timewait函式,讓這個執行緒等待一會兒,留出足夠的時間讓函式pthread_create返回。設定一段等待時間,是在多執行緒程式設計裡常用的方法。但是注意不要使用諸如wait()之類的函式,它們是使整個程式睡眠,並不能解決執行緒同步的問題。
另外一個可能常用的屬 *** 是執行緒的優先順序,它存放在結構sched_param中。用函式pthread_attr_getschedparam和函式 pthread_attr_setschedparam進行存放,一般說來,我們總是先取優先順序,對取得的值修改後再存放回去。下面即是一段簡單的例子。
#include <pthread.h>
#include <sched.h>
pthread_attr_t attr;
pthread_t tid;
sched_param param;
int newprio=20;
pthread_attr_init(&attr);
pthread_attr_getschedparam(&attr, ¶m);
param.sched_priority=newprio;
pthread_attr_setschedparam(&attr, ¶m);
pthread_create(&tid, &attr, (void *)myfunction, myarg);
4 執行緒的資料處理
和程式相比,執行緒的最大優點之一是資料的共享 *** ,各個程式共享父程式處沿襲的資料段,可以方便的獲得、修改資料。但這也給多執行緒程式設計帶來了許多問題。我們必須當心有多個不同的程式訪問相同的變數。許多函式是不可重入的,即同時不能執行一個函式的多個拷貝(除非使用不同的資料段)。在函式中宣告的靜態變數常常帶來問題,函式的返回值也會有問題。因為如果返回的是函式內部靜態宣告的空間的地址,則在一個執行緒呼叫該函式得到地址後使用該地址指向的資料時,別的執行緒可能呼叫此函式並修改了這一段資料。在程式中共享的變數必須用關鍵字volatile來定義,這是為了防止編譯器在優化時(如gcc中使用-OX引數)改變它們的使用方式。為了保護變數,我們必須使用訊號量、互斥等方法來保證我們對變數的正確使用。下面,我們就逐步介紹處理執行緒資料時的有關知識。
4.1 執行緒資料
在單執行緒的程式裡,有兩種基本的資料:全域性變數和區域性變數。但在多執行緒程式裡,還有第三種資料型別:執行緒資料(TSD: Thread-Specific Data)。它和全域性變數很象,線上程內部,各個函式可以象使用全域性變數一樣呼叫它,但它對執行緒外部的其它執行緒是不可見的。這種資料的必要 *** 是顯而易見的。例如我們常見的變數errno,它返回標準的出錯資訊。它顯然不能是一個區域性變數,幾乎每個函式都應該可以呼叫它;但它又不能是一個全域性變數,否則在A執行緒裡輸出的很可能是B執行緒的出錯資訊。要實現諸如此類的變數,我們就必須使用執行緒資料。我們為每個執行緒資料建立一個鍵,它和這個鍵相關聯,在各個執行緒裡,都使用這個鍵來指代執行緒資料,但在不同的執行緒裡,這個鍵代表的資料是不同的,在同一個執行緒裡,它代表同樣的資料內容。
和執行緒資料相關的函式主要有4個:建立一個鍵;為一個鍵指定執行緒資料;從一個鍵讀取執行緒資料;刪除鍵。
建立鍵的函式原型為:
extern int pthread_key_create __P ((pthread_key_t *__key,
void (*__destr_function) (void *)));
第一個引數為指向一個鍵值的指標,第二個引數指明瞭一個destructor函式,如果這個引數不為空,那麼當每個執行緒結束時,系統將呼叫這個函式來釋放繫結在這個鍵上的記憶體塊。這個函式常和函式pthread_once ((pthread_once_t*once_control, void (*initroutine) (void)))一起使用,為了讓這個鍵只被建立一次。函式pthread_once宣告一個初始化函式,第一次呼叫pthread_once時它執行這個函式,以後的呼叫將被它忽略。
在下面的例子中,我們建立一個鍵,並將它和某個資料相關聯。我們要定義一個函式 createWindow,這個函式定義一個圖形視窗(資料型別為Fl_Window *,這是圖形介面開發工具FLTK中的資料型別)。由於各個執行緒都會呼叫這個函式,所以我們使用執行緒資料。
/* 宣告一個鍵*/
pthread_key_t myWinKey;
/* 函式 createWindow */
void createWindow ( void ) {
Fl_Window * win;
static pthread_once_t>
Posix執行緒程式設計指南(1)
http://www-900.ibm.com/developer ... i/part5/index.shtml
內容:
一、 執行緒建立
二、執行緒取消
關於作者
執行緒建立與取消
楊沙洲(pubb@163.net)
2001 年 10 月
這是一個關於Posix執行緒程式設計的專欄。作者在闡明概念的基礎上,將向您詳細講述Posix執行緒庫API。本文是第一篇將向您講述執行緒的建立與取消。
一、執行緒建立
1.1 執行緒與程式
相對程式而言,執行緒是一個更加接近於執行體的概念,它可以與同程式中的其他執行緒共享資料,但擁有自己的棧空間,擁有獨立的執行序列。在序列程式基礎上引入執行緒和程式是為了提高程式的併發度,從而提高程式執行效率和響應時間。
執行緒和程式在使用上各有優缺點:執行緒執行開銷小,但不利於資源的管理和保護;而程式正相反。同時,執行緒適合於在SMP機器上執行,而程式則可以跨機器遷移。
1.2 建立執行緒
POSIX通過pthread_create()函式建立執行緒,API定義如下:
int pthread_create(pthread_t * thread, pthread_attr_t * attr,
void * (*start_routine)(void *), void * arg)
與 fork()呼叫建立一個程式的方法不同,pthread_create()建立的執行緒並不具備與主執行緒(即呼叫pthread_create()的執行緒)同樣的執行序列,而是使其執行start_routine(arg)函式。thread返回建立的執行緒ID,而attr是建立執行緒時設定的執行緒屬 *** (見下)。pthread_create()的返回值表示執行緒建立是否成功。儘管arg是void *型別的變數,但它同樣可以作為任意型別的引數傳給start_routine()函式;同時,start_routine()可以返回一個void *型別的返回值,而這個返回值也可以是其他型別,並由pthread_join()獲取。
1.3 執行緒建立屬 ***
pthread_create()中的attr引數是一個結構指標,結構中的元素分別對應著新執行緒的執行屬 *** ,主要包括以下幾項:
__detachstate,表示新執行緒是否與程式中其他執行緒脫離同步,如果置位則新執行緒不能用pthread_join()來同步,且在退出時自行釋放所佔用的資源。預設為 PTHREAD_CREATE_JOINABLE狀態。這個屬 *** 也可以線上程建立並執行以後用pthread_detach()來設定,而一旦設定為PTHREAD_CREATE_DETACH狀態(不論是建立時設定還是執行時設定)則不能再恢復到PTHREAD_CREATE_JOINABLE狀態。
__schedpolicy,表示新執行緒的排程策略,主要包括SCHED_OTHER(正常、非實時)、SCHED_RR(實時、輪轉法)和SCHED_FIFO(實時、先入先出)三種,預設為 SCHED_OTHER,後兩種排程策略僅對超級使用者有效。執行時可以用過pthread_setschedparam()來改變。
__schedparam,一個struct sched_param結構,目前僅有一個sched_priority整型變數表示執行緒的執行優先順序。這個引數僅當排程策略為實時(即SCHED_RR 或SCHED_FIFO)時才有效,並可以在執行時通過pthread_setschedparam()函式來改變,預設為0。
__inheritsched,有兩種值可供選擇:PTHREAD_EXPLICIT_SCHED和PTHREAD_INHERIT_SCHED,前者表示新執行緒使用顯式指定排程策略和排程引數(即attr中的值),而後者表示繼承呼叫者執行緒的值。預設為PTHREAD_EXPLICIT_SCHED。
__scope,表示執行緒間競爭CPU的範圍,也就是說執行緒優先順序的有效範圍。POSIX的標準中定義了兩個值:PTHREAD_SCOPE_SYSTEM和 PTHREAD_SCOPE_PROCESS,前者表示與系統中所有執行緒一起競爭CPU時間,後者表示僅與同程式中的執行緒競爭CPU。目前 LinuxThreads僅實現了PTHREAD_SCOPE_SYSTEM一值。
pthread_attr_t結構中還有一些值,但不使用pthread_create()來設定。
為了設定這些屬 *** ,POSIX定義了一系列屬 *** 設定函式,包括pthread_attr_init()、pthread_attr_destroy()和與各個屬 *** 相關的pthread_attr_get---/pthread_attr_set---函式。
1.4 執行緒建立的Linux實現
我們知道,Linux的執行緒實現是在核外進行的,核內提供的是建立程式的介面do_fork()。核心提供了兩個系統呼叫__clone()和 fork(),最終都用不同的引數呼叫do_fork()核內API。當然,要想實現執行緒,沒有核心對多程式(其實是輕量級程式)共享資料段的支援是不行的,因此,do_fork()提供了很多引數,包括CLONE_VM(共享記憶體空間)、CLONE_FS(共享檔案系統資訊)、 CLONE_FILES(共享檔案描述符表)、CLONE_SIGHAND(共享訊號控制程式碼表)和CLONE_PID(共享程式ID,僅對核內程式,即0號程式有效)。當使用fork系統呼叫時,核心呼叫do_fork()不使用任何共享屬 *** ,程式擁有獨立的執行環境,而使用pthread_create()來建立執行緒時,則最終設定了所有這些屬 *** 來呼叫__clone(),而這些引數又全部傳給核內的do_fork(),從而建立的"程式"擁有共享的執行環境,只有棧是獨立的,由 __clone()傳入。
Linux執行緒在核內是以輕量級程式的形式存在的,擁有獨立的程式表項,而所有的建立、同步、刪除等 *** 作都在核外pthread庫中進行。pthread庫使用一個管理執行緒(__pthread_manager(),每個程式獨立且唯一)來管理執行緒的建立和終止,為執行緒分配執行緒ID,傳送執行緒相關的訊號(比如Cancel),而主執行緒(pthread_create())的呼叫者則通過管道將請求資訊傳給管理執行緒。
二、執行緒取消
2.1 執行緒取消的定義
一般情況下,執行緒在其主體函式退出的時候會自動終止,但同時也可以因為接收到另一個執行緒發來的終止(取消)請求而強制終止。
2.2 執行緒取消的語義
執行緒取消的方法是向目標執行緒發Cancel訊號,但如何處理Cancel訊號則由目標執行緒自己決定,或者忽略、或者立即終止、或者繼續執行至Cancelation-point(取消點),由不同的Cancelation狀態決定。
執行緒接收到CANCEL訊號的預設處理(即pthread_create()建立執行緒的預設狀態)是繼續執行至取消點,也就是說設定一個CANCELED狀態,執行緒繼續執行,只有執行至Cancelation-point的時候才會退出。
2.3 取消點
根據POSIX標準,pthread_join()、pthread_testcancel()、pthread_cond_wait()、 pthread_cond_timedwait()、sem_wait()、sigwait()等函式以及read()、write()等會引起阻塞的系統呼叫都是Cancelation-point,而其他pthread函式都不會引起Cancelation動作。但是pthread_cancel的手冊頁聲稱,由於LinuxThread庫與C庫結合得不好,因而目前C庫函式都不是Cancelation-point;但CANCEL訊號會使執行緒從阻塞的系統呼叫中退出,並置EINTR錯誤 *** ,因此可以在需要作為Cancelation-point的系統呼叫前後呼叫pthread_testcancel(),從而達到POSIX標準所要求的目標,即如下代 *** 段:
pthread_testcancel();
retcode = read(fd, buffer, length);
pthread_testcancel();
2.4 程式設計方面的考慮
如果執行緒處於無限迴圈中,且迴圈體內沒有執行至取消點的必然路徑,則執行緒無法由外部其他執行緒的取消請求而終止。因此在這樣的迴圈體的必經路徑上應該加入pthread_testcancel()呼叫。
2.5 與執行緒取消相關的pthread函式
int pthread_cancel(pthread_t thread)
傳送終止訊號給thread執行緒,如果成功則返回0,否則為非0值。傳送成功並不意味著thread會終止。
int pthread_setcancelstate(int state, int *oldstate)
設定本執行緒對Cancel訊號的反應,state有兩種值:PTHREAD_CANCEL_ENABLE(預設)和 PTHREAD_CANCEL_DISABLE,分別表示收到訊號後設為CANCLED狀態和忽略CANCEL訊號繼續執行;old_state如果不為 NULL則存入原來的Cancel狀態以便恢復。
int pthread_setcanceltype(int type, int *oldtype)
設定本執行緒取消動作的執行時機,type由兩種取值:PTHREAD_CANCEL_DEFFERED和 PTHREAD_CANCEL_ASYCHRONOUS,僅當Cancel狀態為Enable時有效,分別表示收到訊號後繼續執行至下一個取消點再退出和立即執行取消動作(退出);oldtype如果不為NULL則存入運來的取消動作型別值。
void pthread_testcancel(void)
檢查本執行緒是否處於Canceld狀態,如果是,則進行取消動作,否則直接返回。
關於作者
楊沙洲,男,現攻讀國防科大計算機學院計算機軟體方向博士學位。您可以通過電子郵件pubb@163.net 跟他聯絡。
(c) Copyright IBM Corp. 2001, (c) Copyright IBM China 2001, All Right Reserved
關於 IBM | 隱私條約 | 使用條款 | 聯絡 IBM
-------------------------------------------------------------------------
Posix執行緒程式設計指南(2)
內容:
一. 概念及作用
二. 建立和登出
三. 訪問
四. 使用範例
關於作者
相關內容:
(1) 執行緒建立與取消
執行緒私有資料
楊沙洲(pubb@163.net)
2001 年 10 月
這是一個關於Posix執行緒程式設計的專欄。作者在闡明概念的基礎上,將向您詳細講述Posix執行緒庫API。本文是第二篇將向您講述執行緒的私有資料。
一.概念及作用
在單執行緒程式中,我們經常要用到"全域性變數"以實現多個函式間共享資料。在多執行緒環境下,由於資料空間是共享的,因此全域性變數也為所有執行緒所共有。但有時應用程式設計中有必要提供執行緒私有的全域性變數,僅在某個執行緒中有效,但卻可以跨多個函式訪問,比如程式可能需要每個執行緒維護一個連結串列,而使用相同的函式 *** 作,最簡單的辦法就是使用同名而不同變數地址的執行緒相關資料結構。這樣的資料結構可以由Posix執行緒庫維護,稱為執行緒私有資料(Thread- specific Data,或TSD)。
二.建立和登出
Posix定義了兩個API分別用來建立和登出TSD:
int pthread_key_create(pthread_key_t *key, void (*destr_function) (void *))
該函式從TSD池中分配一項,將其值賦給key供以後訪問使用。如果destr_function不為空,線上程退出(pthread_exit())時將以key所關聯的資料為引數呼叫destr_function(),以釋放分配的緩衝區。
不論哪個執行緒呼叫pthread_key_create(),所建立的key都是所有執行緒可訪問的,但各個執行緒可根據自己的需要往key中填入不同的值,這就相當於提供了一個同名而不同值的全域性變數。在LinuxThreads的實現中,TSD池用一個結構陣列表示:
static struct pthread_key_struct pthread_keys[PTHREAD_KEYS_MAX] = { { 0, NULL } };
建立一個TSD就相當於將結構陣列中的某一項設定為"in_use",並將其索引返回給*key,然後設定destructor函式為destr_function。
登出一個TSD採用如下API:
int pthread_key_delete(pthread_key_t key)
這個函式並不檢查當前是否有執行緒正使用該TSD,也不會呼叫清理函式(destr_function),而只是將TSD釋放以供下一次呼叫 pthread_key_create()使用。在LinuxThreads中,它還會將與之相關的執行緒資料項設為NULL(見"訪問")。
三.訪問
TSD的讀寫都通過專門的Posix Thread函式進行,其API定義如下:
int pthread_setspecific(pthread_key_t key, const void *pointer)
void * pthread_getspecific(pthread_key_t key)
寫入(pthread_setspecific())時,將pointer的值(不是所指的內容)與key相關聯,而相應的讀出函式則將與key相關聯的資料讀出來。資料型別都設為void *,因此可以指向任何型別的資料。
在LinuxThreads中,使用了一個位於執行緒描述結構(_pthread_descr_struct)中的二維void *指標陣列來存放與key關聯的資料,陣列大小由以下幾個巨集來說明:
#define PTHREAD_KEY_2NDLEVEL_SIZE 32
#define PTHREAD_KEY_1STLEVEL_SIZE \
((PTHREAD_KEYS_MAX + PTHREAD_KEY_2NDLEVEL_SIZE - 1)
/ PTHREAD_KEY_2NDLEVEL_SIZE)
其中在/usr/include/bits/local_lim.h中定義了PTHREAD_KEYS_MAX為1024,因此一維陣列大小為32。而具體存放的位置由key值經過以下計算得到:
idx1st = key / PTHREAD_KEY_2NDLEVEL_SIZE
idx2nd = key % PTHREAD_KEY_2NDLEVEL_SIZE
也就是說,資料存放與一個32×32的稀疏矩陣中。同樣,訪問的時候也由key值經過類似計算得到資料所在位置索引,再取出其中內容返回。
四.使用範例
以下這個例子沒有什麼實際意義,只是說明如何使用,以及能夠使用這一機制達到儲存執行緒私有資料的目的。
#include <stdio.h>
#include <pthread.h>
pthread_key_t key;
void echomsg(int t)
{
printf("destructor excuted in thread %d,param=%d\n",pthread_self(),t);
}
void * child1(void *arg)
{
int tid=pthread_self();
printf("thread %d enter\n",tid);
pthread_setspecific(key,(void *)tid);
sleep(2);
printf("thread %d returns %d\n",tid,pthread_getspecific(key));
sleep(5);
}
void * child2(void *arg)
{
int tid=pthread_self();
printf("thread %d enter\n",tid);
pthread_setspecific(key,(void *)tid);
sleep(1);
printf("thread %d returns %d\n",tid,pthread_getspecific(key));
sleep(5);
}
int main(void)
{
int tid1,tid2;
printf("hello\n");
pthread_key_create(&key,echomsg);
pthread_create(&tid1,NULL,child1,NULL);
pthread_create(&tid2,NULL,child2,NULL);
sleep(10);
pthread_key_delete(key);
printf("main thread exit\n");
return 0;
}
給例程建立兩個執行緒分別設定同一個執行緒私有資料為自己的執行緒ID,為了檢驗其私有 *** ,程式錯開了兩個執行緒私有資料的寫入和讀出的時間,從程式執行結果可以看出,兩個執行緒對TSD的修改互不干擾。同時,當執行緒退出時,清理函式會自動執行,引數為tid。
Posix執行緒程式設計指南(3)
內容:
一. 互斥鎖
二. 條件變數
三. 訊號燈
四. 非同步訊號
五. 其他同步方式
關於作者
相關內容:
(1) 執行緒建立與取消
(2) 執行緒私有資料
執行緒同步
楊沙洲(pubb@163.net)
2001 年 10 月
這是一個關於Posix執行緒程式設計的專欄。作者在闡明概念的基礎上,將向您詳細講述Posix執行緒庫API。本文是第三篇,將向您講述執行緒同步。
一.互斥鎖
儘管在Posix Thread中同樣可以使用IPC的訊號量機制來實現互斥鎖mutex功能,但顯然semphore的功能過於強大了,在Posix Thread中定義了另外一套專門用於執行緒同步的mutex函式。
1. 建立和銷燬
有兩種方法建立互斥鎖,靜態方式和動態方式。POSIX定義了一個巨集PTHREAD_MUTEX_INITIALIZER來靜態初始化互斥鎖,方法如下:
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
在LinuxThreads實現中,pthread_mutex_t是一個結構,而PTHREAD_MUTEX_INITIALIZER則是一個結構常量。
動態方式是採用pthread_mutex_init()函式來初始化互斥鎖,API定義如下:
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
其中mutexattr用於指定互斥鎖屬 *** (見下),如果為NULL則使用預設屬 *** 。
pthread_mutex_destroy()用於登出一個互斥鎖,API定義如下:
int pthread_mutex_destroy(pthread_mutex_t *mutex)
銷燬一個互斥鎖即意味著釋放它所佔用的資源,且要求鎖當前處於開放狀態。由於在Linux中,互斥鎖並不佔用任何資源,因此LinuxThreads中的pthread_mutex_destroy()除了檢查鎖狀態以外(鎖定狀態則返回EBUSY)沒有其他動作。
2. 互斥鎖屬 ***
互斥鎖的屬 *** 在建立鎖的時候指定,在LinuxThreads實現中僅有一個鎖型別屬 *** ,不同的鎖型別在試圖對一個已經被鎖定的互斥鎖加鎖時表現不同。當前(glibc2.2.3,linuxthreads0.9)有四個值可供選擇:
PTHREAD_MUTEX_TIMED_NP,這是預設值,也就是普通鎖。當一個執行緒加鎖以後,其餘請求鎖的執行緒將形成一個等待佇列,並在解鎖後按優先順序獲得鎖。這種鎖策略保證了資源分配的公平 *** 。
PTHREAD_MUTEX_RECURSIVE_NP,巢狀鎖,允許同一個執行緒對同一個鎖成功獲得多次,並通過多次unlock解鎖。如果是不同執行緒請求,則在加鎖執行緒解鎖時重新競爭。
PTHREAD_MUTEX_ERRORCHECK_NP,檢錯鎖,如果同一個執行緒請求同一個鎖,則返回EDEADLK,否則與PTHREAD_MUTEX_TIMED_NP型別動作相同。這樣就保證當不允許多次加鎖時不會出現最簡單情況下的死鎖。
PTHREAD_MUTEX_ADAPTIVE_NP,適應鎖,動作最簡單的鎖型別,僅等待解鎖後重新競爭。
3. 鎖 *** 作
鎖 *** 作主要包括加鎖pthread_mutex_lock()、解鎖pthread_mutex_unlock()和測試加鎖 pthread_mutex_trylock()三個,不論哪種型別的鎖,都不可能被兩個不同的執行緒同時得到,而必須等待解鎖。對於普通鎖和適應鎖型別,解鎖者可以是同程式內任何執行緒;而檢錯鎖則必須由加鎖者解鎖才有效,否則返回EPERM;對於巢狀鎖,文件和實現要求必須由加鎖者解鎖,但實驗結果表明並沒有這種 *** ,這個不同目前還沒有得到解釋。在同一程式中的執行緒,如果加鎖後沒有解鎖,則任何其他執行緒都無法再獲得鎖。
int pthread_mutex_lock(pthread_mutex_t *mutex)
int pthread_mutex_unlock(pthread_mutex_t *mutex)
int pthread_mutex_trylock(pthread_mutex_t *mutex)
pthread_mutex_trylock()語義與pthread_mutex_lock()類似,不同的是在鎖已經被佔據時返回EBUSY而不是掛起等待。
4. 其他
POSIX 執行緒鎖機制的Linux實現都不是取消點,因此,延遲取消型別的執行緒不會因收到取消訊號而離開加鎖等待。值得注意的是,如果執行緒在加鎖後解鎖前被取消,鎖將永遠保持鎖定狀態,因此如果在關鍵區段內有取消點存在,或者設定了非同步取消型別,則必須在退出回撥函式中解鎖。
這個鎖機制同時也不是非同步訊號安全的,也就是說,不應該在訊號處理過程中使用互斥鎖,否則容易造成死鎖。
二.條件變數
條件變數是利用執行緒間共享的全域性變數進行同步的一種機制,主要包括兩個動作:一個執行緒等待"條件變數的條件成立"而掛起;另一個執行緒使"條件成立"(給出條件成立訊號)。為了防止競爭,條件變數的使用總是和一個互斥鎖結合在一起。
1. 建立和登出
條件變數和互斥鎖一樣,都有靜態動態兩種建立方式,靜態方式使用PTHREAD_COND_INITIALIZER常量,如下:
pthread_cond_t cond=PTHREAD_COND_INITIALIZER
動態方式呼叫pthread_cond_init()函式,API定義如下:
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr)
儘管POSIX標準中為條件變數定義了屬 *** ,但在LinuxThreads中沒有實現,因此cond_attr值通常為NULL,且被忽略。
登出一個條件變數需要呼叫pthread_cond_destroy(),只有在沒有執行緒在該條件變數上等待的時候才能登出這個條件變數,否則返回EBUSY。因為Linux實現的條件變數沒有分配什麼資源,所以登出動作只包括檢查是否有等待執行緒。API定義如下:
int pthread_cond_destroy(pthread_cond_t *cond)
2. 等待和激發
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime)
等待條件有兩種方式:無條件等待pthread_cond_wait()和計時等待pthread_cond_timedwait(),其中計時等待方式如果在給定時刻前條件沒有滿足,則返回ETIMEOUT,結束等待,其中abstime以與time()系統呼叫相同意義的絕對時間形式出現,0表示格林尼治時間1970年1月1日0時0分0秒。
無論哪種等待方式,都必須和一個互斥鎖配合,以防止多個執行緒同時請求pthread_cond_wait()(或 pthread_cond_timedwait(),下同)的競爭條件(Race Condition)。mutex互斥鎖必須是普通鎖(PTHREAD_MUTEX_TIMED_NP)或者適應鎖(PTHREAD_MUTEX_ADAPTIVE_NP),且在呼叫pthread_cond_wait()前必須由本執行緒加鎖(pthread_mutex_lock()),而在更新條件等待佇列以前,mutex保持鎖定狀態,並線上程掛起進入等待前解鎖。在條件滿足從而離開 pthread_cond_wait()之前,mutex將被重新加鎖,以與進入pthread_cond_wait()前的加鎖動作對應。
激發條件有兩種形式,pthread_cond_signal()啟用一個等待該條件的執行緒,存在多個等待執行緒時按入隊順序啟用其中一個;而pthread_cond_broadcast()則啟用所有等待執行緒。
3. 其他
pthread_cond_wait() 和pthread_cond_timedwait()都被實現為取消點,因此,在該處等待的執行緒將立即重新執行,在重新鎖定mutex後離開 pthread_cond_wait(),然後執行取消動作。也就是說如果pthread_cond_wait()被取消,mutex是保持鎖定狀態的,因而需要定義退出回撥函式來為其解鎖。
以下示例集中演示了互斥鎖和條件變數的結合使用,以及取消對於條件等待動作的影響。在例子中,有兩個執行緒被啟動,並等待同一個條件變數,如果不使用退出回撥函式(見範例中的註釋部分),則tid2將在pthread_mutex_lock()處永久等待。如果使用回撥函式,則tid2的條件等待及主執行緒的條件激發都能正常工作。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex;
pthread_cond_t cond;
void * child1(void *arg)
{
pthread_cleanup_push(pthread_mutex_unlock,&mutex); /* comment 1 */
while(1){
printf("thread 1 get running \n");
printf("thread 1 pthread_mutex_lock returns %d\n",
pthread_mutex_lock(&mutex));
pthread_cond_wait(&cond,&mutex);
printf("thread 1 condition applied\n");
pthread_mutex_unlock(&mutex);
sleep(5);
}
pthread_cleanup_pop(0); /* comment 2 */
}
void *child2(void *arg)
{
while(1){
sleep(3); /* comment 3 */
printf("thread 2 get running.\n");
printf("thread 2 pthread_mutex_lock returns %d\n",
pthread_mutex_lock(&mutex));
pthread_cond_wait(&cond,&mutex);
printf("thread 2 condition applied\n");
pthread_mutex_unlock(&mutex);
sleep(1);
}
}
int main(void)
{
int tid1,tid2;
printf("hello, condition variable test\n");
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&cond,NULL);
pthread_create(&tid1,NULL,child1,NULL);
pthread_create(&tid2,NULL,child2,NULL);
do{
sleep(2); /* comment 4 */
pthread_cancel(tid1); /* comment 5 */
sleep(2); /* comment 6 */
pthread_cond_signal(&cond);
}while(1);
sleep(100);
pthread_exit(0);
}
如果不做註釋5的pthread_cancel()動作,即使沒有那些sleep()延時 *** 作,child1和child2都能正常工作。註釋3和註釋4的延遲使得child1有時間完成取消動作,從而使child2能在child1退出之後進入請求鎖 *** 作。如果沒有註釋1和註釋2的回撥函式定義,系統將掛起在child2請求鎖的地方;而如果同時也不做註釋3和註釋4的延時,child2能在 child1完成取消動作以前得到控制,從而順利執行申請鎖的 *** 作,但卻可能掛起在pthread_cond_wait()中,因為其中也有申請mutex的 *** 作。child1函式給出的是標準的條件變數的使用方式:回撥函式保護,等待條件前鎖定,pthread_cond_wait()返回後解鎖。
條件變數機制不是非同步訊號安全的,也就是說,在訊號處理函式中呼叫pthread_cond_signal()或者pthread_cond_broadcast()很可能引起死鎖。
三.訊號燈
訊號燈與互斥鎖和條件變數的主要不同在於"燈"的概念,燈亮則意味著資源可用,燈滅則意味著不可用。如果說後兩中同步方式側重於"等待" *** 作,即資源不可用的話,訊號燈機制則側重於點燈,即告知資源可用;沒有等待執行緒的解鎖或激發條件都是沒有意義的,而沒有等待燈亮的執行緒的點燈 *** 作則有效,且能保持燈亮狀態。當然,這樣的 *** 作原語也意味著更多的開銷。
訊號燈的應用除了燈亮/燈滅這種二元燈以外,也可以採用大於1的燈數,以表示資源數大於1,這時可以稱之為多元燈。
1. 建立和登出
POSIX訊號燈標準定義了有名訊號燈和無名訊號燈兩種,但LinuxThreads的實現僅有無名燈,同時有名燈除了總是可用於多程式之間以外,在使用上與無名燈並沒有很大的區別,因此下面僅就無名燈進行討論。
int sem_init(sem_t *sem, int pshared, unsigned int value)
這是建立訊號燈的API,其中value為訊號燈的初值,pshared表示是否為多程式共享而不僅僅是用於一個程式。LinuxThreads沒有實現多程式共享訊號燈,因此所有非0值的pshared輸入都將使sem_init()返回-1,且置errno為ENOSYS。初始化好的訊號燈由sem變數表徵,用於以下點燈、滅燈 *** 作。
int sem_destroy(sem_t * sem)
被登出的訊號燈sem要求已沒有執行緒在等待該訊號燈,否則返回-1,且置errno為EBUSY。除此之外,LinuxThreads的訊號燈登出函式不做其他動作。
2. 點燈和滅燈
int sem_post(sem_t * sem)
點燈 *** 作將訊號燈值原子地加1,表示增加一個可訪問的資源。
int sem_wait(sem_t * sem)
int sem_trywait(sem_t * sem)
sem_wait()為等待燈亮 *** 作,等待燈亮(訊號燈值大於0),然後將訊號燈原子地減1,並返回。sem_trywait()為sem_wait()的非阻塞版,如果訊號燈計數大於0,則原子地減1並返回0,否則立即返回-1,errno置為EAGAIN。
3. 獲取燈值
int sem_getvalue(sem_t * sem, int * sval)
讀取sem中的燈計數,存於*sval中,並返回0。
4. 其他
sem_wait()被實現為取消點,而且在支援原子"比較且交換"指令的體系結構上,sem_post()是唯一能用於非同步訊號處理函式的POSIX非同步訊號安全的API。
四.非同步訊號
由於LinuxThreads是在核外使用核內輕量級程式實現的執行緒,所以基於核心的非同步訊號 *** 作對於執行緒也是有效的。但同時,由於非同步訊號總是實際發往某個程式,所以無法實現POSIX標準所要求的"訊號到達某個程式,然後再由該程式將訊號分發到所有沒有阻塞該訊號的執行緒中"原語,而是隻能影響到其中一個執行緒。
POSIX非同步訊號同時也是一個標準C庫提供的功能,主要包括訊號集管理(sigemptyset()、sigfillset()、sigaddset()、sigdelset()、sigismember()等)、訊號處理函式安裝(sigaction())、訊號阻塞控制(sigprocmask())、被阻塞訊號查詢(sigpending())、訊號等待 (sigsuspend())等,它們與傳送訊號的kill()等函式配合就能實現程式間非同步訊號功能。LinuxThreads圍繞執行緒封裝了 sigaction()和raise(),本節集中討論LinuxThreads中擴充套件的非同步訊號函式,包括pthread_sigmask()、 pthread_kill()和sigwait()三個函式。毫無疑問,所有POSIX非同步訊號函式對於執行緒都是可用的。
int pthread_sigmask(int how, const sigset_t *newmask, sigset_t *oldmask)
設定執行緒的訊號遮蔽 *** ,語義與sigprocmask()相同,但對不允許遮蔽的Cancel訊號和不允許響應的Restart訊號進行了保護。被遮蔽的訊號儲存在訊號佇列中,可由sigpending()函式取出。
int pthread_kill(pthread_t thread, int signo)
向thread號執行緒傳送signo訊號。實現中在通過thread執行緒號定位到對應程式號以後使用kill()系統呼叫完成傳送。
int sigwait(const sigset_t *set, int *sig)
掛起執行緒,等待set中指定的訊號之一到達,並將到達的訊號存入*sig中。POSIX標準建議在呼叫sigwait()等待訊號以前,程式中所有執行緒都應遮蔽該訊號,以保證僅有sigwait()的呼叫者獲得該訊號,因此,對於需要等待同步的非同步訊號,總是應該在建立任何執行緒以前呼叫 pthread_sigmask()遮蔽該訊號的處理。而且,呼叫sigwait()期間,原來附接在該訊號上的訊號處理函式不會被呼叫。
如果在等待期間接收到Cancel訊號,則立即退出等待,也就是說sigwait()被實現為取消點。
五.其他同步方式
除了上述討論的同步方式以外,其他很多程式間通訊手段對於LinuxThreads也是可用的,比如基於檔案系統的IPC(管道、Unix域Socket 等)、訊息佇列(Sys.V或者Posix的)、System V的訊號燈等。只有一點需要注意,LinuxThreads在核內是作為共享儲存區、共享檔案系統屬 *** 、共享訊號處理、共享檔案描述符的獨立程式看待的。
Posix執行緒程式設計指南(4)
內容:
1. 執行緒終止方式
2. 執行緒終止時的清理
3. 執行緒終止的同步及其返回值
4. 關於pthread_exit()和return
參考資料
關於作者
相關內容:
(1) 執行緒建立與取消
(2) 執行緒私有資料
(3) 執行緒同步
執行緒終止
楊沙洲(pubb@163.net)
2001 年 11 月
這是一個關於Posix執行緒程式設計的專欄。作者在闡明概念的基礎上,將向您詳細講述Posix執行緒庫API。本文是第四篇將向您講述執行緒中止。
1.執行緒終止方式
一般來說,Posix的執行緒終止有兩種情況:正常終止和非正常終止。執行緒主動呼叫pthread_exit()或者從執行緒函式中return都將使執行緒正常退出,這是可預見的退出方式;非正常終止是執行緒在其他執行緒的干預下,或者由於自身執行出錯(比如訪問非法地址)而退出,這種退出方式是不可預見的。
2.執行緒終止時的清理
不論是可預見的執行緒終止還是異常終止,都會存在資源釋放的問題,在不考慮因執行出錯而退出的前提下,如何保證執行緒終止時能順利的釋放掉自己所佔用的資源,特別是鎖資源,就是一個必須考慮解決的問題。
最經常出現的情形是資源獨佔鎖的使用:執行緒為了訪問臨界資源而為其加上鎖,但在訪問過程中被外界取消,如果執行緒處於響應取消狀態,且採用非同步方式響應,或者在開啟獨佔鎖以前的執行路徑上存在取消點,則該臨界資源將永遠處於鎖定狀態得不到釋放。外界取消 *** 作是不可預見的,因此的確需要一個機制來簡化用於資源釋放的程式設計。
在POSIX執行緒API中提供了一個 pthread_cleanup_push()/pthread_cleanup_pop()函式對用於自動釋放資源--從 pthread_cleanup_push()的呼叫點到pthread_cleanup_pop()之間的程式段中的終止動作(包括呼叫 pthread_exit()和取消點終止)都將執行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_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);
/* do some work */
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);
}
3.執行緒終止的同步及其返回值
一般情況下,程式中各個執行緒的執行都是相互獨立的,執行緒的終止並不會通知,也不會影響其他執行緒,終止的執行緒所佔用的資源也並不會隨著執行緒的終止而得到釋放。正如程式之間可以用wait()系統呼叫來同步終止並釋放資源一樣,執行緒之間也有類似機制,那就是pthread_join()函式。
void pthread_exit(void *retval)
int pthread_join(pthread_t th, void **thread_return)
int pthread_detach(pthread_t th)
pthread_join() 的呼叫者將掛起並等待th執行緒終止,retval是pthread_exit()呼叫者執行緒(執行緒ID為th)的返回值,如果thread_return 不為NULL,則*thread_return=retval。需要注意的是一個執行緒僅允許唯一的一個執行緒使用pthread_join()等待它的終止,並且被等待的執行緒應該處於可join狀態,即非DETACHED狀態。
如果程式中的某個執行緒執行了 pthread_detach(th),則th執行緒將處於DETACHED狀態,這使得th執行緒在結束執行時自行釋放所佔用的記憶體資源,同時也無法由 pthread_join()同步,pthread_detach()執行之後,對th請求pthread_join()將返回錯誤。
一個可join的執行緒所佔用的記憶體僅當有執行緒對其執行了pthread_join()後才會釋放,因此為了避免記憶體洩漏,所有執行緒的終止,要麼已設為DETACHED,要麼就需要使用pthread_join()來回收。
4.關於pthread_exit()和return
理論上說,pthread_exit()和執行緒宿體函式退出的功能是相同的,函式結束時會在內部自動呼叫pthread_exit()來清理執行緒相關的資源。但實際上二者由於編譯器的處理有很大的不同。
在程式主函式(main())中呼叫pthread_exit(),只會使主函式所在的執行緒(可以說是程式的主執行緒)退出;而如果是return,編譯器將使其呼叫程式退出的代 *** (如_exit()),從而導致程式及其所有執行緒結束執行。
其次,線上程宿主函式中主動呼叫return,如果return語句包含在pthread_cleanup_push()/pthread_cleanup_pop()對中,則不會引起清理函式的執行,反而會導致segment fault。
Posix執行緒程式設計指南(5)
內容:
1.獲得本執行緒ID
2.判斷兩個執行緒是否為同一執行緒
3.僅執行一次的 *** 作
4.pthread_kill_other_threads_np()
關於作者
相關內容:
(1) 執行緒建立與取消
(2) 執行緒私有資料
(3) 執行緒同步
(4) 執行緒終止
雜項
楊沙洲(pubb@163.net)
2001 年 11 月
這是一個關於Posix執行緒程式設計的專欄。作者在闡明概念的基礎上,將向您詳細講述Posix執行緒庫API。本文是第五篇將向您講述pthread_self()、pthread_equal()和pthread_once()等雜項函式。
在 Posix執行緒規範中還有幾個輔助函式難以歸類,暫且稱其為雜項函式,主要包括pthread_self()、pthread_equal()和 pthread_once()三個,另外還有一個LinuxThreads非可移植 *** 擴充套件函式pthread_kill_other_threads_np()。本文就介紹這幾個函式的定義和使用。
1.獲得本執行緒ID
pthread_t pthread_self(void)
本函式返回本執行緒的識別符號。
在LinuxThreads中,每個執行緒都用一個pthread_descr結構來描述,其中包含了執行緒狀態、執行緒ID等所有需要的資料結構,此函式的實現就是線上程棧幀中找到本執行緒的pthread_descr結構,然後返回其中的p_tid項。
pthread_t型別在LinuxThreads中定義為無符號長整型。
2.判斷兩個執行緒是否為同一執行緒
int pthread_equal(pthread_t thread1, pthread_t thread2)
判斷兩個執行緒描述符是否指向同一執行緒。在LinuxThreads中,執行緒ID相同的執行緒必然是同一個執行緒,因此,這個函式的實現僅僅判斷thread1和thread2是否相等。
3.僅執行一次的 *** 作
int pthread_once(pthread_once_t *once_control, void (*init_routine) (void))
本函式使用初值為PTHREAD_ONCE_INIT的once_control變數保證init_routine()函式在本程式執行序列中僅執行一次。
#include <stdio.h>
#include <pthread.h>
pthread_once_t once=PTHREAD_ONCE_INIT;
void >
相關文章
- 多執行緒學習一(多執行緒基礎)執行緒
- Java多執行緒學習——執行緒通訊Java執行緒
- Java多執行緒學習(2)執行緒控制Java執行緒
- 多執行緒學習(二)執行緒
- Java多執行緒學習Java執行緒
- 多執行緒學習一執行緒
- iOS 多執行緒-學習iOS執行緒
- Java多執行緒學習(一)Java多執行緒入門Java執行緒
- #大學#Java多執行緒學習02(執行緒同步)Java執行緒
- Java 多執行緒NIO學習Java執行緒
- C++多執行緒學習C++執行緒
- Java多執行緒(學習篇)Java執行緒
- 多執行緒學習筆記執行緒筆記
- 多執行緒學習初步(轉)執行緒
- C#多執行緒學習(四) 多執行緒的自動管理(執行緒池)C#執行緒
- C# 多執行緒學習(4) :多執行緒的自動管理(執行緒池)C#執行緒
- Java多執行緒學習(3)執行緒同步與執行緒通訊Java執行緒
- C#多執行緒學習(一) 多執行緒的相關概念C#執行緒
- C# 多執行緒學習(1) :多執行緒的相關概念C#執行緒
- Linux學習--執行緒控制Linux執行緒
- Java 多執行緒學習筆記Java執行緒筆記
- 多執行緒學習-Disruptor佇列執行緒佇列
- java學習筆記--多執行緒Java筆記執行緒
- java多執行緒學習小案例Java執行緒
- 多執行緒學習筆記 (轉)執行緒筆記
- Java多執行緒學習(八)執行緒池與Executor 框架Java執行緒框架
- Java 多執行緒學習筆記(三)-守護執行緒Java執行緒筆記
- 多執行緒基礎必要知識點!看了學習多執行緒事半功倍執行緒
- Java多執行緒學習(1)建立執行緒與執行緒的生命週期Java執行緒
- 多執行緒Demo學習(執行緒的同步,簡單的執行緒通訊)執行緒
- python 多程式和多執行緒學習Python執行緒
- 關於linux多執行緒fork的理解和學習Linux執行緒
- 多執行緒複習執行緒
- Python學習筆記 - 多執行緒Python筆記執行緒
- Java多執行緒學習筆記(自用)Java執行緒筆記
- 多執行緒學習(二)CountDownLunch與CyclicBarrier執行緒
- C#多執行緒學習筆記C#執行緒筆記
- 4、Linux多執行緒,執行緒同步(2)Linux執行緒