Linux线程详解
并行和并發的區別
1. 并發(concurrency):在操作系統中,是指一個時間段中有幾個程序都處于已啟動運行到運行完畢之間,且這幾個程序都是在同一個處理機上運行。其中兩種并發關系分別是同步和互斥。(并發是指同一時刻只能有一條指令執行,但多個進程指令被快速輪換執行,使得在宏觀上有多個進程被同時執行的效果--宏觀上并行,針對單核處理器)
- 互斥:進程間相互排斥的使用臨界資源的現象,就叫互斥。
- 同步(synchronous):進程之間的關系不是相互排斥臨界資源的關系,而是相互依賴的關系。進一步的說明:就是前一個進程的輸出作為后一個進程的輸入,當第一個進程沒有輸出時第二個進程必須等待。具有同步關系的一組并發進程相互發送的信息稱為消息或事件。(彼此有依賴關系的調用不應該同時發生,而同步就是阻止那些“同時發生”的事情)
- 其中并發又有偽并發和真并發,偽并發是指單核處理器的并發,真并發是指多核處理器的并發。
2.并行(parallelism):在單處理器中多道程序設計系統中,進程被交替執行,表現出一種并發的外部特種;在多處理器系統中,進程不僅可以交替執行,而且可以重疊執行。在多處理器上的程序才可實現并行處理。從而可知,并行是針對多處理器而言的。并行是同時發生的多個并發事件,具有并發的含義,但并發不一定并行,也亦是說并發事件之間不一定要同一時刻發生。(同一時刻,有多條指令在多個處理器上同時執行--針對多核處理器)
同步和異步的區別
- 同步(synchronous):進程之間的關系不是相互排斥臨界資源的關系,而是相互依賴的關系。進一步的說明:就是前一個進程的輸出作為后一個進程的輸入,當第一個進程沒有輸出時第二個進程必須等待。具有同步關系的一組并發進程相互發送的信息稱為消息或事件。
- 異步(asynchronous):異步和同步是相對的,同步就是順序執行,執行完一個再執行下一個,需要等待、協調運行。異步就是彼此獨立,在等待某事件的過程中繼續做自己的事,不需要等待這一事件完成后再工作。線程就是實現異步的一個方式。異步是讓調用方法的主線程不需要同步等待另一線程的完成,從而可以讓主線程干其它的事情。
線程概念
什么是線程
- LWP:light weight process 輕量級的進程,本質仍是進程(在Linux環境下)
- 進程:獨立地址空間,擁有PCB
- 線程:也有PCB,但沒有獨立的地址空間(共享)
- 區別:在于是否共享地址空間。 獨居(進程);合租(線程)。
- Linux下: 線程:最小的執行單位,調度的基本單位。
- 進程:最小分配資源單位,可看成是只有一個線程的進程。
Linux內核線程實現原理
類Unix系統中,早期是沒有“線程”概念的,80年代才引入,借助進程機制實現出了線程的概念。因此在這類系統中,進程和線程關系密切。
- 輕量級進程(light-weight process),也有PCB,創建線程使用的底層函數和進程一樣,都是clone。
- 從內核里看進程和線程是一樣的,都有各自不同的PCB,但是PCB中指向內存資源的三級頁表是相同的。
- 進程可以蛻變成線程
- 線程可看做寄存器和棧的集合
- 在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() 函數。
- pthread_t pthread_self(void); 返回值:成功:0; 失敗:無!
- 線程ID:pthread_t類型,本質:在Linux下為無符號整數(%lu),其他系統中可能是結構體實現
- 線程ID是進程內部,識別標志。(兩個進程間,線程ID允許相同)
- 注意:不應使用全局變量 pthread_t tid,在子線程中通過pthread_create傳出參數來獲取線程ID,而應使用pthread_self。
pthread_create函數? 創建一個新線程。 其作用,對應進程中fork() 函數。
- 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。
練習:循環創建多個線程,每個線程打印自己是第幾個被創建的線程。(類似于進程循環創建子進程)
#include <pthread.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h>void *tfn(void *arg) {int i;i = (int)arg;//i = *((int *)arg);sleep(i); //通過i來區別每個線程printf("I'm %dth thread, Thread_ID = %lu\n", i+1, pthread_self());return NULL; }int main(int argc, char *argv[]) {int n, 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("I am main, and I am not a process, I'm a thread!\n" "main_thread_ID = %lu\n", pthread_self());return 0; }運行結果
線程與共享? 線程間共享全局變量!
- 【牢記】:線程默認共享數據段、代碼段等地址空間,常用的是全局變量。而進程不共享全局變量,只能借助mmap。
設計程序,驗證線程之間共享全局數據。
#include <stdio.h> #include <pthread.h> #include <stdlib.h> #include <unistd.h>int var = 100;void *tfn(void *arg) {var = 200;printf("thread\n");return NULL; }int main(void) {printf("At first var = %d\n", var);pthread_t tid;pthread_create(&tid, NULL, tfn, NULL);sleep(1);printf("after pthread_create, var = %d\n", var);return 0; }運行結果
pthread_exit函數? 將單個線程退出
- void pthread_exit(void *retval); 參數:retval表示線程退出狀態,通常傳NULL
運行結果
思考:使用exit將指定線程退出,可以嗎??結論:線程中,禁止使用exit函數,會導致進程內所有線程全部退出。
- 在不添加sleep控制輸出順序的情況下。pthread_create在循環中,幾乎瞬間創建5個線程,但只有第1個線程有機會輸出(或者第2個也有,也可能沒有,取決于內核調度)如果第3個線程執行了exit,將整個進程退出了,所以全部線程退出了。
- 所以,多線程環境中,應盡量少用,或者不使用exit函數,取而代之使用pthread_exit函數,將單個線程退出。任何線程里exit導致進程退出,其他線程未工作結束,主控線程退出時不能return或exit。
- 另注意,pthread_exit或者return返回的指針所指向的內存單元必須是全局的或者是用malloc分配的,不能在線程函數的棧上分配,因為當其它線程得到這個返回指針時線程函數已經退出了。
pthread_join函數? 阻塞等待線程退出,獲取線程退出狀態 其作用,對應進程中 waitpid() 函數。
- int pthread_join(pthread_t thread, void **retval); 成功:0;失敗:錯誤號
- 參數:thread:線程ID (【注意】:不是指針);retval:存儲線程結束狀態。
對比記憶:
參數 retval 非空用法
調用該函數的線程將掛起等待,直到id為thread的線程終止。thread線程以不同的方法終止,通過pthread_join得到的終止狀態是不同的,總結如下:
練習:使用pthread_join函數將循環創建的多個子線程回收。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h>int var = 100;void *tfn(void *arg) {int i;i = (int)arg;sleep(i);if (i == 1) {var = 333;printf("var = %d\n", var);return (void *)var;}else if (i == 3) {var = 777;printf("I'm %dth pthread, pthread_id = %lu\n var = %d\n", i+1, pthread_self(), var);pthread_exit((void *)var);} else {printf("I'm %dth pthread, pthread_id = %lu\n var = %d\n", i+1, pthread_self(), var);pthread_exit((void *)var);}return NULL; }int main(void) {pthread_t tid[5];int i;int *ret[5]; for (i = 0; i < 5; i++)pthread_create(&tid[i], NULL, tfn, (void *)i);for (i = 0; i < 5; i++) {pthread_join(tid[i], (void **)&ret[i]);printf("-------%d 's ret = %d\n", i, (int)ret[i]);}printf("I'm main pthread tid = %lu\t var = %d\n", pthread_self(), var);sleep(i);return 0; }運行結果
pthread_cancel函數? 殺死(取消)線程 其作用,對應進程中 kill() 函數。
- int pthread_cancel(pthread_t thread); 成功:0;失敗:錯誤號
運行結果
- 線程的取消并不是實時的,而有一定的延時。需要等待線程到達某個取消點(檢查點)。
- 類似于玩游戲存檔,必須到達指定的場所(存檔點,如:客棧、倉庫、城里等)才能存儲進度。殺死線程也不是立刻就能完成,必須要到達取消點。
- 取消點:是線程檢查是否被取消,并按請求進行動作的一個位置。通常是一些系統調用creat,open,pause,close,read,write.....?執行命令man?7 pthreads可以查看具備這些取消點的系統調用列表。也可參閱 APUE.12.7 取消選項小節。
- 可粗略認為一個系統調用(進入內核)即為一個取消點。如線程中沒有取消點,可以通過調用pthreestcancel函數自行設置一個取消點。
- 被取消的線程, 退出值定義在Linux的pthread庫中。常數PTHREAD_CANCELED的值是-1。可在頭文件pthread.h中找到它的定義:#define PTHREAD_CANCELED ((void *) -1)。因此當我們對一個已經被取消的線程使用pthread_join回收時,得到的返回值為-1。
pthread_detach函數? 實現線程分離
- int pthread_detach(pthread_t thread); 成功:0;失敗:錯誤號
- 線程分離狀態:指定該狀態,線程主動與主控線程斷開關系。線程結束后,其退出狀態不由其他線程獲取,而直接自己自動釋放。網絡、多線程服務器常用。
- 在線程被分離后,不能使用pthread_join等待它的終止狀態。
- 進程若有該機制,將不會產生僵尸進程。僵尸進程的產生主要由于進程死后,大部分資源被釋放,一點殘留資源仍存于系統中,導致內核認為該進程仍存在。
- 也可使用 pthread_create函數參2(線程屬性)來設置線程分離。
使用pthread_detach函數實現線程分離?
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <pthread.h>void *tfn(void *arg) {int n = 3;while (n--) {printf("thread count %d\n", n);sleep(1); }return (void *)1;//pthread_exit((void *)1); }int main(void) {pthread_t tid;void *tret;int err;#if 1pthread_attr_t attr; /*通過線程屬性來設置游離態*/pthread_attr_init(&attr);pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);pthread_create(&tid, &attr, tfn, NULL);#elsepthread_create(&tid, NULL, tfn, NULL);pthread_detach(tid); //讓線程分離 ----自動退出,無系統殘留資源#endifwhile (1) {err = pthread_join(tid, &tret);printf("thread exit code = %d\n", (int)tret);printf("-------------err= %d\n", err);if (err != 0)fprintf(stderr, "thread_join error: %s\n\n", strerror(err));elsefprintf(stderr, "thread exit code %d\n\n", (int)tret);sleep(1);}return 0; }運行結果
- 一般情況下,線程終止后,其終止狀態一直保留到其它線程調用pthread_join獲取它的狀態為止。但是線程也可以被置為detach狀態,這樣的線程一旦終止就立刻回收它占用的所有資源,而不保留終止狀態。不能對一個已經處于detach狀態的線程調用pthread_join,這樣的調用將返回EINVAL錯誤。也就是說,如果已經對一個線程調用了pthread_detach就不能再調用pthread_join了。
終止線程方式
總結:終止某個線程而不終止整個進程,有三種方法:
控制原語對比
進程? ? ? ? ? ? 線程
fork? ? ? ? ? ? ?pthread_create
exit? ? ? ? ? ? ?pthread_exit
wait? ? ? ? ? ? pthread_join
kill? ? ? ? ? ? ? pthread_cancel
getpid? ? ? ? pthread_self 命名空間
?
總結
- 上一篇: 红色警戒 direct错误
- 下一篇: Altium Designer软件绘图