注意:使用線程庫函數用gcc編譯時,要加參數:-lpthread(libpthread.so),因為線程庫函數屬于第三方c庫函數,不是標準庫函數(/lib、/usr/lib或者/usr/local/lib)。
(1)pthread_self函數
獲取線程ID。其作用對應進程中 getpid() 函數。
pthread_t pthread_self(void);?? 返回值:成功:調用該函數的線程ID;失敗:永遠不會失敗。
pthread_t:typedef? unsigned long int? pthread_t;? 輸出格式格式為:%lu
線程ID是進程內部,識別標志。(兩個進程間,線程ID允許相同)
注意:不應使用全局變量 pthread_t tid(因為全局變量被一個進程中的多個線程共享),在子線程中通過pthread_create傳出參數來獲取線程ID或者使用pthread_self。
(2)pthread_create函數
創建一個新線程。其作用對應進程中fork() 函數。
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
返回值:成功:0;失敗:錯誤編號。Linux環境下,所有線程特點,失敗均直接返回錯誤編號,即直接返回errno number。
參數1:傳出參數,新建線程的線程ID
參數2:傳入參數,通常傳NULL,表示使用線程默認屬性。若想使用具體屬性也可以修改該參數。
參數3:函數指針,指向線程主控函數(線程體),該函數運行結束,則線程結束。
參數4:線程主函數執行期間所使用的參數,若不同參數傳NULL。
在一個線程中調用pthread_create()創建新的線程后,當前線程從pthread_create()返回繼續往下執行,而新的線程所執行的代碼由我們傳給pthread_create的函數指針start_routine決定(線程創建成功后就立即去執行start_routine函數)。start_routine函數接收一個參數,是通過pthread_create的arg參數傳遞給它的,該參數的類型為void *,這個指針按什么類型解釋由調用者自己定義。start_routine的返回值類型也是void *,這個指針的含義同樣由調用者自己定義。start_routine返回時,這個線程就退出了,其它線程可以調用pthread_join得到start_routine的返回值,類似于父進程調用wait( )得到子進程的退出狀態,稍后詳細介紹pthread_join。
pthread_create成功返回后,新創建的線程的id被填寫到thread參數所指向的內存單元。我們知道進程id的類型是pid_t,每個進程的id在整個系統中是唯一的,調用getpid( )可以獲得當前進程的id,是一個正整數值。線程id的類型是pthread_t,它只在當前進程中保證是唯一的,在不同的系統中pthread_t這個類型有不同的實現,它可能是一個整數值,也可能是一個結構體,也可能是一個地址,所以不能簡單地當成整數(%lu)用printf打印,調用pthread_self( )可以獲得當前線程的id。
attr參數表示線程屬性,本節不深入討論線程屬性,所有代碼例子都傳NULL給attr參數,表示線程屬性取缺省值,感興趣的讀者可以參考APUE。
//pthread_self和pthread_create函數示例
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>void *tfn(void *arg)
{printf("In thread, the process id is %d and thread id is %lu.\n",getpid( ),pthread_self( ));return NULL;
}int main(void)
{pthread_t tid;int ret;printf("In main1, the process id is %d and thread id is %lu.\n",getpid( ),pthread_self( ));ret = pthread_create(&tid, NULL, tfn, NULL);if( ret != 0 ) //出錯判斷{fprintf(stderr,"pthread_create error: %s\n",strerror(ret));exit(1);}printf("the created thread's id is %lu\n",tid);sleep(1); //注意一定要睡1sprintf("In main2, the process id is %u and thread id is %lu.\n",getpid( ),pthread_self( ));return 0;
}
[root@localhost 01_pthread_test]# ./pthrd_crt
In main1, the process id is 9814 and thread id is 4151932608.
the created thread's id is 4149869376
In thread, the process id is 9814 and thread id is 4149869376.
In main2, the process id is 9814 and thread id is 4151932608.
分析:
- 在main函數中調用sleep函數的原因。在Linux環境中,進程的本質就是含有一個線程的進程,其在創建線程之前就包含一個線程,因此此時有一個進程ID和一個線程ID(當然也有一個線程號),兩個ID不想等,將此時的線程稱為主控線程,后面創建的線程稱為子線程。主線程創建完子線程后,如果主線程提前就結束了(執行了return或者exit等語句),則此時進程的地址空間會被回收,因此子線程也就被終止了(因為線程共享地址空間)。因此,主線程(main函數)必須要睡1s等待子線程完成工作后再結束。
- 注意線程庫函數調用失敗返回值的判斷。出錯不會再置ennro的值為相應的值,而是直接輸出錯誤編號,因此不能再用perror函數來輸出詳細錯誤信息(該函數是根據errno的值輸出相應的錯誤信息)。可以采用strerror函數,其作用:返回一個字符串,這個字符串描述了錯誤編號所對應的錯誤信息。char * strerror(int errnum);???? 如上所示:fprintf(stderr,"pthread_create error: %s\n",strerror(ret)); 采用fprintf函數將錯誤信息輸出。相對于printf,fprintf函數可以將內容輸出到指定文件中,第一個參數為文件的結構體指針。printf與fprintf都是標準輸出,stdin、stdout和stderr為標準輸入、標準輸出和標準錯誤輸出對應的文件結構體。
- 輸出結果可以看出:在創建子線程前,進程本身包含一個線程;同一個進程中的所有線程的進程ID都一樣;線程ID明顯比線程號和進程ID大得多。
總結:由于pthread_create的錯誤碼不保存在errno中,因此不能直接用perror( )打印錯誤信息,可以先用strerror( )把錯誤碼轉換成錯誤信息再打印。為了防止新創建的線程還沒有得到執行就終止,我們在main函數return之前延時1秒,這只是一種權宜之計,即使主線程等待1秒,內核也不一定會調度新創建的線程執行,后面會有更好的辦法。
//循環創建n個子線程的架構
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>void *tfn(void *arg)
{int s = (int)arg;sleep(s);printf("The %dth thread: the process id is %d and thread id is %lu.\n",s+1,getpid( ),pthread_self( ));return NULL;
}int main(void)
{pthread_t tid;int ret, i;for( i=0;i<5;i++ ){ret = pthread_create(&tid, NULL, tfn,(void *)i);if( ret != 0 ){fprintf(stderr,"pthread_create error: %s\n",strerror(ret));exit(1);}}sleep(i);printf("In main: the process id is %u and thread id is %lu.\n",getpid( ),pthread_self( ));return 0;
}
[root@localhost 01_pthread_test]# ./pthrd_crt
The 1th thread: the process id is 4026 and thread id is 4149246784.
The 2th thread: the process id is 4026 and thread id is 4140854080.
The 3th thread: the process id is 4026 and thread id is 4132461376.
The 4th thread: the process id is 4026 and thread id is 4124068672.
The 5th thread: the process id is 4026 and thread id is 4115675968.
In main: the process id is 4026 and thread id is 4151466240.
分析:
- 如上循環創建5個線程,該進程中有6個線程,進程ID均相同,線程ID不一樣;
- 每一個進程中線程的數目是有限的,不能超過這個值;
- 特別強調:主控線程調用的pthread_create函數中的第4個參數(void *)i不能改為:(void *)&i,相應在子線程執行的函數tfn中為s = *( (int *)arg ); 詳細分析如下:首先,main函數是主控線程執行的函數,因此位于主控線程的用戶棧幀空間中,該空間保存了main函數中的局部變量和形參值:tid、red、i,當main函數又去調用pthread_create函數時,main函數的棧幀頭尾作為臨時值保存在棧幀空間中。pthread_create函數的形參值保存在它的棧幀空間中。對于tfn函數(子線程執行的函數),它的局部變量和形參值(s和arg)又保存在各個線程的用戶棧空間中,相互獨立。外界參數的值傳遞給函數的形參時,都是按值傳遞(指針則傳地址值,非指針變量則傳變量值),如果傳參數(void *)i,則將數值i轉變為指針值 (指針值與數值i在大小上是相等的,只是在32位系統中指針值和整型i都占據4個字節;在64位系統中,指針值占據8個字節,而整型i依然占據4個字節,此時高位補0即可。然后將該指針值賦值給指針變量arg,即arg的值大小上是等于i的,但arg位于子線程的用戶棧空間中,然后:(int)arg;即把指針值轉變為整型s,s當然等于i,因此該方式是正確的。在64位系統中,指針值轉變為整型i,8字節變為4字節,截取高位,而高位都是0,因此無影響。但如果是:(void *)&i ?*( (int *)arg),這種情況下傳遞的是i的地址給arg,因此在子線程中是通過i的地址來訪問數值i的,而數值i在主控線程中是會隨時發生變化的(for循環中的i++導致),因此該方式不成立。
- pthread_create函數是一個回調函數,若創建子線程成功,則會去調用相應的函數;
- 子線程執行完自己的函數后返回的值通過pthread_join函數回收;
- return的作用是返回到函數的調用點,如果是main函數中的return,則代表該進程結束,并釋放進程地址空間,所有線程都終止。對于其它函數的return,則直接返回到函數的調用點。exit和_exit函數會直接終止整個進程,導致所有線程結束。pthread_exit函數則會導致調用該函數的線程結束。所以,多線程環境中,應盡量少用,或者不使用exit函數,取而代之使用pthread_exit函數,將單個線程退出。任何線程里exit導致進程退出,其他線程也結束,主控線程退出時不能return或exit。