這里說一下相關的基礎知識:
線程概念
什么是線程
LWP:light weight process 輕量級的進程,本質仍是進程(在Linux環境下)
進程:獨立地址空間,擁有PCB
線程:也有PCB,但沒有獨立的地址空間(共享)
區別:在於是否共享地址空間。 獨居(進程);合租(線程)。
Linux下: 線程:最小的執行單位
進程:最小分配資源單位,可看成是只有一個線程的進程。
Linux內核線程實現原理
類Unix系統中,早期是沒有"線程"概念的,80年代才引入,借助進程機制實現出了線程的概念。因此在這類系統中,進程和線程關系密切。
1. 輕量級進程(light-weight process),也有PCB,創建線程使用的底層函數和進程一樣,都是clone
2. 從內核里看進程和線程是一樣的,都有各自不同的PCB,但是PCB中指向內存資源的三級頁表是相同的
3. 進程可以蛻變成線程
4. 線程可看做寄存器和棧的集合
5. 在linux下,線程最是小的執行單位;進程是最小的分配資源單位
察看LWP號:ps –Lf pid 查看指定線程的lwp號。
三級映射:進程PCB --> 頁目錄(可看成數組,首地址位於PCB中) --> 頁表 --> 物理頁面 --> 內存單元
參考:《Linux內核源代碼情景分析》 ----毛德操
對於進程來說,相同的地址(同一個虛擬地址)在不同的進程中,反復使用而不沖突。原因是他們雖虛擬址一樣,但,頁目錄、頁表、物理頁面各不相同。相同的虛擬址,映射到不同的物理頁面內存單元,最終訪問不同的物理頁面。
但!線程不同!兩個線程具有各自獨立的PCB,但共享同一個頁目錄,也就共享同一個頁表和物理頁面。所以兩個PCB共享一個地址空間。
實際上,無論是創建進程的fork,還是創建線程的pthread_create,底層實現都是調用同一個內核函數clone。
如果復制對方的地址空間,那么就產出一個"進程";如果共享對方的地址空間,就產生一個"線程"。
因此:Linux內核是不區分進程和線程的。只在用戶層面上進行區分。所以,線程所有操作函數 pthread_* 是庫函數,而非系統調用。
線程共享資源
1.文件描述符表
2.每種信號的處理方式
3.當前工作目錄
4.用戶ID和組ID
5.內存地址空間 (.text/.data/.bss/heap/共享庫)
線程非共享資源
1.線程id
2.處理器現場和棧指針(內核棧)
3.獨立的棧空間(用戶空間棧)
4.errno變量
5.信號屏蔽字
6.調度優先級
線程優、缺點
優點: 1. 提高程序並發性 2. 開銷小 3. 數據通信、共享數據方便
缺點: 1. 庫函數,不穩定 2. 調試、編寫困難、gdb不支持 3. 對信號支持不好
優點相對突出,缺點均不是硬傷。Linux下由於實現方法導致進程、線
程差別不是很大。
線程控制原語
pthread_self函數
獲取線程ID。其作用對應進程中 getpid() 函數。
頭文件:#include <pthread.h>
pthread_t pthread_self(void); 返回值:成功:0; 失敗:無!
pthread_t:當前Linux中可理解為:typedef unsigned long int pthread_t;//無符號長整形
線程ID:pthread_t類型,本質:在Linux下為無符號整數(%lu),其他系統中可能是結構體實現
線程ID是進程內部,識別標志。(兩個進程間,線程ID允許相同)
注意:不應使用全局變量 pthread_t tid,在子線程中通過pthread_create傳出參數來獲取線程ID,而應使用pthread_self。
pthread_create函數
創建一個新線程。 其作用,對應進程中fork() 函數。
頭文件:#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
返回值:成功:0; 失敗:錯誤號 -----Linux環境下,所有線程特點,失敗均直接返回錯誤號。
參數:
pthread_t:當前Linux中可理解為:typedef unsigned long int pthread_t;//無符號長整形
參數1:傳出參數,保存系統為我們分配好的線程ID
參數2:通常傳NULL,表示使用線程默認屬性。若想使用具體屬性也可以修改該參數。
參數3:函數指針,指向線程主函數(線程體),該函數運行結束,則線程結束。參數是函數指針,只能傳遞函數名,不能傳遞參數。所以就是只能有一個參數。
參數4:線程主函數執行期間所使用的參數。
在一個線程中調用pthread_create()創建新的線程后,當前線程從pthread_create()返回繼續往下執行,而新的線程所執行的代碼由我們傳給pthread_create的函數指針start_routine決定。start_routine函數接收一個參數,是通過pthread_create的arg參數傳遞給它的,該參數的類型為void *,這個指針按什么類型解釋由調用者自己定義。start_routine的返回值類型也是void *,這個指針的含義同樣由調用者自己定義。start_routine返回時,這個線程就退出了,其它線程可以調用pthread_join得到start_routine的返回值,類似於父進程調用wait(2)得到子進程的退出狀態,稍后詳細介紹pthread_join。
pthread_create成功返回后,新創建的線程的id被填寫到thread參數所指向的內存單元。我們知道進程id的類型是pid_t,每個進程的id在整個系統中是唯一的,調用getpid(2)可以獲得當前進程的id,是一個正整數值。線程id的類型是thread_t,它只在當前進程中保證是唯一的,在不同的系統中thread_t這個類型有不同的實現,它可能是一個整數值,也可能是一個結構體,也可能是一個地址,所以不能簡單地當成整數用printf打印,調用pthread_self(3)可以獲得當前線程的id。
attr參數表示線程屬性,本節不深入討論線程屬性,所有代碼例子都傳NULL給attr參數,表示線程屬性取缺省值,感興趣的讀者可以參考APUE。
現在我們先預熱:創建一個新線程,打印線程ID。注意:鏈接線程庫 -lpthread。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void *tfn(void *arg)
{
printf("我是線程,我的ID = %lu\n", pthread_self());
return NULL;
}
int main(void)
{
pthread_t tid;
pthread_create(&tid, NULL, tfn, NULL);
sleep(1);
printf("我是進程,我的進程ID = %d\n", getpid());
return 0;
}
結果:
由於pthread_create的錯誤碼不保存在errno中,因此不能直接用perror(3)打印錯誤信息,可以先用strerror(3)把錯誤碼轉換成錯誤信息再打印。如果任意一個線程調用了exit或_exit,則整個進程的所有線程都終止,由於從main函數return也相當於調用exit,為了防止新創建的線程還沒有得到執行就終止,我們在main函數return之前延時1秒,這只是一種權宜之計,即使主線程等待1秒,內核也不一定會調度新創建的線程執行,下一節我們會看到更好的辦法。要這樣寫命令:gcc -pthread pthread_create.c -o pthread_create
現在進入主題:循環創建多個線程,每個線程打印自己是第幾個被創建的線程。(類似於進程循環創建子進程)
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
void *tfn(void *arg)
{
int i;
i = (int)arg;
sleep(i); //通過i來區別每個線程
printf("我是第%d個線程,我的線程ID = %lu\n", i + 1, pthread_self());
return NULL;
}
int main(int argc, char *argv[])
{
int n = 5, i;
pthread_t tid;
if (argc == 2)
n = atoi(argv[1]);
for (i = 0; i < n; i++) {
pthread_create(&tid, NULL, tfn, (void *)i);
//將i轉換為指針,在tfn中再強轉回整形。
}
sleep(n);
printf("我是main函數,但是我不是進程,我的ID = %lu\n", pthread_self());
return 0;
}
結果:
一切正常,現在我解釋一些代碼: pthread_create(&tid, NULL, tfn, (void *)i);這里的 (void *)i參數應該是指針,但是我們這里是將其強轉為void*類型了,並且編譯過程中也給我警告了:
那么,為何一切正常,是編譯器為我們做了不知名的優化?或者是巧合?實際上,這是因為的我的機器是64位機,如果是在32位機上編譯是沒有這樣的錯誤的。這個警告是在說int和void轉化中的長度不一致(在我的機器上)。void在64位機上是8位,int一般來說都是4位。這在第一次轉化的時候是小變大,會發生補零,在高位上補零;第二次在i = (int)arg;這里發生大變小轉化,會截取,截取高位。所以,實際上對於這個程序來說是沒有影響的。所以那兩個警告是沒有問題的。其他的我相信是沒有什么問題的。
拓展思考:將pthread_create函數參4修改為(void *)&i, 將線程主函數(tfn)內改為 i=*((int *)arg) 是否可以?
即變成這樣: pthread_create(&tid, NULL, tfn, (void *)&i);i=*((int *)arg) ;行不行試試再說:結果也看到了,雖然沒有警告了,但是結果卻不對了。它不從1開始了,一會兒4個線程,一會兒5個線程。這很蛋疼啊:第四個參數應該是指針啊,沒錯啊。可就是不對。其實也很好理解的,線程之間共享一個用戶空間,我們這樣傳遞的是i的地址過去,然后在運行線程主函數的時候依據地址找i的值,那么,問題出現了,cpu是個很快的男人,從main到線程主函數這之間有時間差吧?所以,在那么點時間內,i的值發生改變了。那為什么有時候線程個數不足?上面只要main一結束,管你后面是不是還有線程的,統統殺死。