Linux 循環創建多個線程


這里說一下相關的基礎知識:

線程概念

什么是線程

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位機上編譯是沒有這樣的錯誤的。這個警告是在說intvoid轉化中的長度不一致(在我的機器上)。void64位機上是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一結束,管你后面是不是還有線程的,統統殺死。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM