線程的簡單了解
之前我們了解過 task_struct 是用于描述進程的核心數據結構。它包含了一個進程的所有重要信息,并且在進程的生命周期內保持更新。我們想要獲取進程相關信息往往從這里得到。
- 在Linux中,線程的實現方式與進程類似,每個線程都有一個task_struct結構體,用于存儲線程的信息。線程的task_struct結構體比進程的task_struct結構體要小,包含的信息更少。
- 進程是操作系統資源分配的最小單位,而線程是操作系統調度的最小單位。
- 線程之間的切換通常比進程切換更高效,因為線程共享進程的資源,不需要像進程切換那樣保存和恢復大量的資源信息。
windows中的線程和Linux中線程區別?
在Windows操作系統,內核中有真線程,名為TCB :線程控制塊。需要維護進程與線程之間的調度關系算法,這過于復雜。
?在Linux中,由于線程的控制塊與進程控制塊相似性非常高,所以直接復用了PCB的結構體——task_struct ,用PCB模擬線程的TCB。所以Linux沒有真正意義上的線程,而是用進程方案模擬的線程。這樣做的好處是復用代碼和結構更簡單,好維護,效率更高,也更安全。
線程的特性?
多線程的優點
- 同時執行多個任務: 多線程允許程序同時執行多個任務,而不是按順序一個接一個地執行。
- 提高響應速度: 對于需要處理大量并發請求的程序(如Web服務器),多線程可以顯著提高程序的響應速度。
- 更小開銷,更快的切換: 線程切換的開銷也比進程切換要小,這使得多線程程序可以更高效地進行任務切換。
- 在等待慢速I/O操作結束的同時,程序可執行其他的計算任務
- 計算密集型應用,為了能在多處理器系統上運行,將計算分解到多個線程中實現,可以有效提高計算效率,但是注意:線程不是越多越好,正常情況下最合適的原則是:進程/線程與cpu個數/核數保持一致
多線程的缺點
- 共享資源競爭: 多個線程共享進程的地址空間,當它們同時訪問和修改共享資源時,可能會出現競爭條件,導致數據不一致或程序錯誤。
- 同步機制復雜: 為了解決線程安全問題,需要使用線程同步機制(如互斥鎖、條件變量等),這些機制會增加編程的復雜性,容易出錯。
- 上下文切換開銷: 線程切換需要保存和恢復線程的上下文,這會消耗一定的CPU時間。過多的線程切換可能會降低程序的效率。
- 線程間依賴: 線程之間可能存在依賴關系,一個線程的執行可能會影響到其他線程的執行。如果處理不當,可能會導致程序出現意外錯誤。
- 調試困難:多線程程序的執行順序是不確定的,這使得程序的調試變得更加困難。由于線程的執行受到多種因素的影響,一些錯誤可能很難復現,增加了調試的難度。
PROSIX線程庫
與線程有關的函數構成了一個完整的系列,絕大多數函數的名字都是以“pthread_”打頭的
要使用這些函數庫,要通過引入頭文件?<pthread.h>
并且鏈接這些線程函數庫時要使用編譯器命令的“-lpthread”選項
之前我們使用的都是linux中的基礎標準庫,這些標準庫在編譯的時候會自動幫我們進行鏈接,我們只需要包含一個頭文件,不需要手動鏈接。但是對于線程庫的話默認不會幫我們鏈接,除了需要我們程序中包含對應頭文件,還需要編譯的時候手動鏈接。
包含頭文件和編譯時鏈接區分
- 包含頭文件(#include): 這是在源代碼文件中做的,用于告訴編譯器程序中使用了哪些函數、變量、類型等。頭文件通常包含函數聲明、宏定義、結構體定義等。
- 編譯時鏈接: 這是在編譯命令中做的,用于告訴鏈接器將程序中使用的函數和變量與它們在庫文件中的具體實現鏈接起來。庫文件通常包含編譯好的函數和變量的二進制代碼。
包含頭文件的作用:
- 讓編譯器理解代碼: 頭文件相當于一個“接口說明書”,告訴編譯器程序中使用了哪些“零件”(函數、變量等),以及這些“零件”的規格(參數類型、返回值類型等)。
- 提供類型檢查: 編譯器可以根據頭文件中的聲明來檢查程序中函數和變量的使用是否正確,避免類型錯誤。
編譯時鏈接的作用:
- 生成可執行文件: 鏈接器將程序中使用的函數和變量與它們在庫文件中的實現“組裝”起來,生成最終的可執行文件。
- 鏈接外部代碼: 程序中使用的某些函數和變量可能不是由自己編寫的,而是由其他人或組織提供的,這些代碼通常放在庫文件中。鏈接器將這些外部代碼鏈接到程序中,使得程序可以使用這些外部功能。
線程創建
pthread_create函數介紹
函數作用:創建一個新的線程。這個線程在創建后會并行執行指定的線程函數
頭文件:#include <pthread.h>
函數原型:
int pthread_create(pthread_t *thread,const pthread_attr_t *attr,void *(*start_routine) (void *),void *arg);
參數:
- thread: 一個指向
pthread_t
類型變量的指針,用于存儲新創建線程的 ID。 - attr: 一個指向
pthread_attr_t
類型變量的指針,用于設置新線程的屬性。如果設置為NULL
,則使用默認屬性。通常我們設置成NULL就行 - start_routine: 一個函數指針,表示新線程要執行的函數。該函數必須接受一個
void *
類型的參數,并返回一個void *
類型的值。 - arg: 一個指向
void *
類型變量的指針,表示傳遞給start_routine
線程函數的參數。如果我們不需要傳遞任何數據給線程函數,完全可以將它設置為 NULL
返回值:
- 成功: 返回 0。
- 失敗: 返回一個非零的錯誤碼,表示創建線程失敗的原因。
線程函數的定義:
線程函數必須符合 void *(*start_routine)(void *)
的函數簽名,即接收一個 void *
類型的參數并返回一個 void *
類型的值。
簡單的線程創建例子
例子比較簡單,主要就是創建了一個新的線程,然后主線程和新線程同時執行,主線程輸出26個英文字母,新線程輸出數字0-9。
編譯代碼的時候記得加上編譯鏈接選項:-lpthread
gcc a.c -o a -lpthread
#include <stdio.h>
#include <pthread.h>void *thread_function(void *arg) {int i=0;while(1){fprintf(stderr,"%d",i);i++;if(i==10){i=0;}}
}int main() {pthread_t thread_id;int ret = pthread_create(&thread_id, NULL, thread_function, NULL);if (ret != 0) {perror("pthread_create failed");return 1;}printf("Thread created successfully\n");int i=0;while(1){fprintf(stderr,"%c",'a'+i);i++;if(i==26){i=0;}}return 0;
}
最終看到的效果就是主線程和新線程雙線執行輸出。
線程間共享資源與不共享資源
共享資源
- 堆(Heap): 存儲進程中動態分配的對象。
- 代碼段(Code Segment): 存儲程序的指令。
- 數據段(Data Segment): 存儲進程的全局變量和靜態變量。
- 文件描述符表: 存儲進程打開文件的信息。
- 信號處理函數: 用于處理進程接收到的信號。
不共享資源
- 棧(Stack): 每個線程都有自己的棧,用于存儲局部變量、函數調用信息等。
- 寄存器(Registers): 每個線程都有一組寄存器,用于存儲線程執行過程中的臨時數據。
- 線程 ID: 每個線程都有一個唯一的線程 ID,用于標識線程。
使用共享資源時需要注意的問題:
- 競態條件(Race Condition): 多個線程同時訪問和修改共享資源時,可能會導致數據不一致的問題。
- 死鎖(Deadlock): 多個線程互相等待對方釋放資源,導致程序無法繼續執行的問題。
解決競態條件和死鎖問題的方法:
- 互斥鎖(Mutex): 用于保護共享資源,同一時刻只允許一個線程訪問。
- 條件變量(Condition Variable): 用于線程之間的同步,當一個線程等待某個條件滿足時,可以使用條件變量進行阻塞。
- 信號量(Semaphore): 用于控制同時訪問共享資源的線程數量。
多線程共享資源同時訪問出錯例子
場景:假設有一個共享的計數器變量 counter
,初始值為 0。現在有多個線程同時對 counter
進行加 1 操作。
預期結果:由于有 10 個線程,每個線程執行 100000 次加 1 操作,因此最終的 counter
值應該為 10 * 100000 = 1000000。
實際結果:實際運行結果通常會小于 10000。
原因分析:
當多個線程同時訪問 counter
變量時,由于線程切換的存在,可能會導致以下情況:
- 線程 A 讀取
counter
的值。 - 線程 A 被切換出去,線程 B 開始執行。
- 線程 B 讀取
counter
的值。 - 線程 B 將
counter
的值加 1。 - 線程 B 被切換出去,線程 A 繼續執行。
- 線程 A 將之前讀取的
counter
值加 1,并寫回。
這樣,線程 A 和線程 B 都只進行了一次加 1 操作,但 counter
的值只增加了 1,而不是 2。這種情況稱為競態條件。
解決方法:可以使用互斥鎖(Mutex)來保護共享資源 counter
,確保同一時刻只有一個線程可以訪問它。
通過使用互斥鎖,可以保證每個線程對 counter
的加 1 操作都是原子性的,從而避免競態條件,得到正確的結果。
#include <stdio.h>
#include <pthread.h>#define NUM_THREADS 10
#define INCREMENTS 100000int counter = 0;void *increment_counter(void *arg) {for (int i = 0; i < INCREMENTS; i++) {counter++;}return NULL;
}int main() {pthread_t threads[NUM_THREADS];for (int i = 0; i < NUM_THREADS; i++) {pthread_create(&threads[i], NULL, increment_counter, NULL);}//作用是等待多個線程執行結束。它通常出現在多線程程序中,//用于確保主線程在所有子線程完成任務后才退出。for (int i = 0; i < NUM_THREADS; i++) {pthread_join(threads[i], NULL);}printf("Expected counter value: %d\n", NUM_THREADS * INCREMENTS);printf("Actual counter value: %d\n", counter);return 0;
}
線程退出?
pthread_exit 函數介紹
?函數作用:結束調用該函數的線程,同時還可以傳遞一個退出狀態值給其他線程
頭文件:#include <pthread.h>
函數原型:
void pthread_exit(void *retval);
參數:
retval 參數可以用于傳遞一個退出狀態值給其他線程,通常通過 pthread_join() 函數來接收這個值。如果不需要傳遞退出狀態,可以將 retval 設置為 NULL。
僵尸線程
首先我們來回顧一下僵尸進程:
僵尸進程
- 當一個進程結束運行時,內核不會立即釋放它占用的所有資源,而是將其狀態設置為僵尸態(Zombie)。
- 僵尸進程會保留一些基本信息(如進程ID、退出狀態等),以便父進程可以獲取到子進程的退出信息。
- 父進程需要調用
wait()
或waitpid()
等函數來回收僵尸進程的資源,否則僵尸進程會一直存在,占用系統資源。
然后我們來看一下線程的僵尸態
與進程類似,當一個線程結束運行時,它也會進入一個類似于僵尸態的狀態。
- 狀態保留: 線程退出后,其占用的大部分資源(如棧空間)會被自動回收,但是線程也會保留一些狀態信息,例如退出狀態,以便其他線程(通常是主線程)可以通過?
pthread_join()
函數來獲取。
?- 回收方式: 線程的“回收”主要通過
pthread_join()
函數來實現。當主線程調用pthread_join()
函數等待某個線程結束時,實際上就是在“回收”該線程的狀態信息。
僵尸態的重要性?
- 無論是進程還是線程,僵尸態的存在都是為了讓父進程或主線程能夠獲取到子進程或子線程的退出信息。
- 這些退出信息可能包含執行結果、錯誤碼等,對于程序的調試和錯誤處理非常有幫助。
線程接合
pthread_join函數介紹
?函數作用:阻塞當前線程,直到指定的線程執行完畢,適用于線程間的同步
頭文件:#include <pthread.h>
函數原型:
int pthread_join(pthread_t thread, void **retval);
參數:
thread:要等待的線程的線程 ID。這是一個由 pthread_create 創建的線程 ID。
retval:這是一個指向指針的指針,函數會把目標線程的退出狀態通過該指針返回。如果目標線程沒有返回任何值,可以傳遞 NULL。
其實我蠻不理解這里為什么使用二級指針的,在我看來pthread_exit 傳遞的參數是一個一級指針,但是這里pthread_join選擇一個一級指針來對應賦值就可以了,不太理解為什么要使用二級指針
?
返回值:
- 成功: 返回 0。
- 失敗: 返回一個非零的錯誤碼。
適用場景
- 同步線程: 當一個線程需要等待另一個線程完成后才能繼續執行時,可以使用
pthread_join()
函數進行同步。 - 獲取線程返回值: 有些線程會返回一個值,表示它們的執行結果。可以使用
pthread_join()
函數獲取這個返回值。 - 資源回收: 當一個線程結束后,它的資源不會立即被釋放。需要調用
pthread_join()
函數才能回收這些資源。
?線程分離態(了解)
線程的默認狀態
默認情況下,新創建的線程都處于非分離態 (Joinable State)。這意味著:
- 資源回收: 當一個線程結束運行時,它所占用的資源(如棧空間)不會立即被釋放,而是會保留一段時間,直到有其他線程調用
pthread_join()
函數來“回收”該線程。 - 獲取退出狀態: 其他線程可以通過調用
pthread_join()
函數來等待該線程結束,并獲取它的退出狀態。
什么是分離態?
分離態 (Detached State) 是一種特殊的線程狀態。當一個線程被設置為分離態時,它與創建它的線程(通常是主線程)之間的關系就會被“分離”。這意味著:
- 自動資源回收: 當一個分離態線程結束運行時,它所占用的資源會被自動回收,無需其他線程調用
pthread_join()
函數。 - 無法獲取退出狀態: 其他線程無法通過
pthread_join()
函數來等待分離態線程的結束,也無法獲取它的退出狀態。
如何設置線程為分離態?
方法一
pthread_detach(thread_id);
這個函數可以直接將指定線程設置為分離狀態。
但是你可能會想如何獲得一個線程自身的線程tid呢?其實很簡單,有一個函數pthread_self可以很容易幫我們獲取到當前線程的tid。
這個函數在后面一篇文章中也會詳細講到,這里只是簡單提一下。
函數原型:pthread_t pthread_self(void);
所以我們常常將 pthread_self 配合 pthread_detach 一起使用,像下面這樣:
pthread_detach(pthread_self());
方法二
使用pthread_create函數創建線程的時候,有一個參數可以設置新創建的線程的屬性。我們可以憑借這個參數來設置線程為分離態,這種方式相比于方法一,更加麻煩,但是有著自己的優點,之后我們會詳細講到,這里不詳細闡述。
適用場景
有些情況下,我們對于某些線程來說不關心它的返回狀態,并且也不想要使用pthread_join來阻塞等待回收這個死后的僵尸線程。那么此時我們就可以把這個線程設置成分離態度,當線程死亡自動釋放,不需要其他線程調用pthread_join來回收這個僵尸進程的資源。