文章目錄
- 1. 線程概念
- 什么是線程
- Linux中的線程
- 線程的優點
- 線程的缺點
- 線程的獨立資源和共享資源
- 2. 線程控制
- Linux的pthread庫
- 用戶級線程
- 📝 個人主頁 :超人不會飛)
- 📑 本文收錄專欄:《Linux》
- 💭 如果本文對您有幫助,不妨點贊、收藏、關注支持博主,我們一起進步,共同成長!
1. 線程概念
什么是線程
💭理解線程需要和進程的概念緊密聯系。
- 線程是一個執行分支,執行粒度比進程更細,調度成本更低;
- 進程是分配系統資源的基本單位,線程是CPU調度的基本單位。
- 線程是運行在進程中的一個執行流,本質上是在進程的地址空間中運行,一個進程至少包含一個線程,稱為主線程。
Linux中的線程
線程是操作系統中的抽象概念,用于實現多任務并發執行。不同的操作系統可以有不同的線程實現方法和模型。例如,在Windows操作系統中,與進程PCB對標的,構建了描述線程的數據結構 —— 線程控制塊,但這樣子設計有以下幾個缺點:
- 創建線程在Windows中開銷較大,因為它涉及到較多的內核資源和數據結構的分配
- 線程與進程無法統一組織起來
- 線程的調度效率低
Linux的設計者發現,線程控制塊與進程控制塊(PCB)大部分描述屬性相同,且進程與其內部創建的線程看到的都是同一個地址空間。因此,在Linux中,線程控制塊直接復用了PCB的代碼,也就是說,Linux底層并沒有真正的“線程”,這種復用之后的線程稱之為輕量級進程。
- 每個輕量級進程(后面直接稱為線程)都有自己的一個編號——LWP,同一個進程中的各個線程具有相同的PID。
🔎那我們之前討論的進程是什么?這里都是輕量級進程的話,需要另有一個進程PCB來管理整個進程嗎?
答案是不用。事實上,在Linux中,因為每個進程都至少有一個線程,即主線程(主執行流),這個線程的LWP和PID是相同的,因此,我們之前討論的進程PCB,實際上就是這個主線程的task_struct。
ps -aL
命令查看系統中的輕量級進程。
測試:在一個進程中,創建了10個線程,并用ps -aL
命令查看。可以看到有一個主線程和10個新線程,主線程的PID和LWP相同。
-
線程的調度成本低于進程,是因為同一個進程中的線程共享同一個地址空間,因此這些線程的調度只需要保存和更改一些上下文信息、CPU寄存器即可,如pc指針。而進程的調度需要修改較多的內存資源,如頁表、地址空間等,而開銷更大的是修改cache緩存的數據。
cache緩存
CPU內部的高速存儲器中,保存著一些頻繁訪問的指令和數據,基于局部性原理,這些數據可能是未來將要被訪問的,也可能是當前正在訪問的。這么做的目的是減少CPU與內存的IO次數,以便快速響應CPU的請求,而不必每次都從較慢的內存中獲取數據。不同進程的cache緩存數據是不同的,因此調度進程是需要切換這部分數據,而同一個進程的不同線程的cache緩存相同。
CPU根據PID和LWP的對比,區分當前調度是線程級還是進程級,進而執行對應的調度策略。
線程的優點
- 線程占用的資源比進程少很多,因此創建線程的開銷比創建進程小
- 線程的調度成本低于進程調度,線程切換時OS的工作量小
- 充分利用多處理器的可并行數量
- 在等待慢速I/O操作結束的同時,程序可執行其他的計算任務
- 計算密集型應用,為了能在多處理器系統上運行,將計算分解到多個線程中實現
- I/O密集型應用,為了提高性能,將I/O操作重疊。線程可以同時等待不同的I/O操作。
線程的缺點
- 性能損失。 一個很少被外部事件阻塞的計算密集型線程往往無法與其它線程共享同一個處理器。 如果計算密集型線程的數量比可用的處理器多,那么可能會有較大的性能損失,這里的性能損失指的是增加了額外的同步和調度開銷,而可用的資源不變。 例如有10個處理器,11個線程,一對一的關系被破壞后,多出來的線程就增加了額外的調度開銷。
- 復雜性和錯誤難以調試。 多線程編程涉及到共享資源、并發訪問和同步等問題,這增加了程序的復雜性。
- 健壯性降低。 編寫多線程需要更全面更深入的考慮,在一個多線程程序里,因時間分配上的細微偏差或者因共享了不該共享的變量而造成不良影響的可能性是很大的,換句話說多線程之間是缺乏保護的。
?補充:
線程發生異常(如野指針、除零錯誤等),會導致線程崩潰,進而引發整個進程退出。從宏觀角度,因為線程是進程的一個執行分支,線程干的事就是進程干的事,因此線程異常相當于進程異常,進程就會退出。從內核角度,線程出錯,OS發送信號給進程,而不是單發給線程。
線程的獨立資源和共享資源
進程是資源分配的基本單位,線程是調度的基本單位。一個進程中的多個線程共享線程數據,當然也有自己獨立的數據。
線程的獨立資源:
- 棧
- 寄存器中的上下文信息
- 線程ID(在Linux中表現為LWP)
- errno
- 信號屏蔽字和未決信號集
- 調度優先級
線程的共享資源:
- 進程地址空間(包括進程的數據段、代碼段等)
- 文件描述符表
- 每種信號的處理方式(SIG_ IGN、SIG_ DFL或者自定義的信號處理函數)
- 當前工作目錄
- 用戶ID和組ID
2. 線程控制
Linux的pthread庫
Liunx中,提供給用戶層進行線程控制的函數被打包在一個動態庫中 —— pthread。使用線程控制接口時,需要包含頭文件
pthread.h
,并在gcc/g++編譯時加上-l pthread
選項確定鏈接動態庫。
在/lib64
目錄下找到pthread庫:
編譯時應該添加的選項:
g++ threadTest.cc -o threadTest -l pthread # -lpthread也可以
-
pthread_create
功能:
? 創建一個線程
接口:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
參數:
thread:線程庫中定義了一個線程ID類型phtread_t,這里的thread是一個輸出型參數,函數會向其指向的空間寫入創建線程的ID
attr:線程的屬性,一般設為nullptr即可
start_routine:線程執行的函數,是一個返回值類型void*,參數類型void*的函數指針
arg:傳入start_routine的參數,使用前后一般需要類型轉換。
返回值:
RETURN VALUEOn success, pthread_create() returns 0; on error, it returns an error number, and the contents of *thread are undefined.
💭關于線程退出的問題:
同子進程退出,需要父進程回收,線程也需要被另外的線程回收。回收的原因如下:1. 一個線程退出后,對應的資源不會被釋放,而是留存在地址空間中。一個進程能運行的線程數是有限的,如果不加以回收,可能會導致內存泄漏!2. 一個線程退出后,其它線程可能需要獲取其執行任務的結果。
-
pthread_join
功能:
? 阻塞等待一個線程
接口:
int pthread_join(pthread_t thread, void **retval);
參數:
thread:線程ID
retval:指向的空間中存儲的是線程返回的結果(注意類型轉換),因為線程函數的返回結果是void*類型,所以要用二級指針接收。如果不關心回收線程的結果,則設置為nullptr。
返回值:
RETURN VALUEOn success, pthread_join() returns 0; on error, it returns an error number.
-
pthread_exit
線程函數中,可以直接用return退出線程并返回結果(可以被其它線程join接收)
void *run(void *arg) {int cnt = 5;while (cnt--){cout << "I am new thread" << endl;sleep(1);}return nullptr; // }
也可以用
pthread_exit
函數。void pthread_exit(void *retval); //和return一樣,返回一個void*指針
Linux中,線程只有joinable和unjoinable兩種狀態。默認情況下,線程是joinable狀態,該狀態下的線程退出后,占有資源不會被釋放,必須等待其它線程調用pthread_join回收它,釋放資源,或者進程退出,資源全部被釋放。當然,可以通過調用pthread_detach分離線程,將線程設置為unjoinable狀態,使其無需被等待回收,退出即被系統自動釋放資源。
-
pthread_detach
功能:
? 分離線程ID為thread的線程,使其無需被join等待。
接口:
int pthread_detach(pthread_t thread);
返回值:
RETURN VALUEOn success, pthread_detach() returns 0; on error, it returns an error number.
線程分離可以由別的線程分離,也可以自己分離。
-
pthread_self
功能:
? 獲取當前線程的線程ID
接口:
pthread_t pthread_self(void);
?測試
void *run(void *arg)
{int cnt = 10;while(cnt--){cout << "I am new thread, cnt: " << cnt << endl;sleep(1);}pthread_exit(nullptr);
}int main()
{cout << "I am main thread" << endl;pthread_t tid;pthread_create(&tid, nullptr, run, nullptr);int n = pthread_join(tid, nullptr);if (n != 0){cout << "join new thread fail!!" << endl;exit(1);}cout << "join new thread success!!" << endl;return 0;
}
主線程創建新線程后,調用pthread_join會阻塞等待新線程退出。運行結果如下:
[ckf@VM-8-3-centos lesson9_thread]$ ./mythread
I am main thread
I am new thread, cnt: 9
I am new thread, cnt: 8
I am new thread, cnt: 7
I am new thread, cnt: 6
I am new thread, cnt: 5
I am new thread, cnt: 4
I am new thread, cnt: 3
I am new thread, cnt: 2
I am new thread, cnt: 1
I am new thread, cnt: 0
join new thread success!!
可以在主線程中detach線程ID為tid的新線程,也可以在新線程中detach自己。
void *run(void *arg)
{//pthread_detach(pthread_self()); // 在新線程中detach自己int cnt = 10;while(cnt--){cout << "I am new thread, cnt: " << cnt << endl;sleep(1);}pthread_exit(nullptr);
}int main()
{cout << "I am main thread" << endl;pthread_t tid;pthread_create(&tid, nullptr, run, nullptr);pthread_detach(tid); // 在主線程中detach線程ID為tid的新線程int n = pthread_join(tid, nullptr);if (n != 0){cout << "join new thread fail!!" << endl;exit(1);}cout << "join new thread success!!" << endl;return 0;
}
[ckf@VM-8-3-centos lesson9_thread]$ ./mythread
I am main thread
join new thread fail!! #等待失敗,pthread_join無法等待已分離的線程,返回值非0
如果在新線程中detach自己,可能依然能夠join成功。要想成功detach線程,必須在join之前detach,因為調用pthread_join函數時,已經將線程視為joinable并阻塞等待了,此后再detach是無效的。上面代碼中,如果在新線程中detach自己,由于主線程和新線程調度的先后順序不確定性,很可能線程先join再detach,此時的detach是無效的。
-
pthread_cancel
功能:
? 撤銷(終止)一個線程ID為thread的線程
接口:
int pthread_cancel(pthread_t thread);
返回值:
RETURN VALUEOn success, pthread_cancel() returns 0; on error, it returns a nonzero error number.
撤銷一個線程后,如果有另外的線程join該線程,那么其收到的退出結果是
PTHREAD_CANCELED
。#define PTHREAD_CANCELED ((void *) -1)
?測試
void *run(void *arg)
{while (true){cout << "I am new thread" << endl;sleep(1);}pthread_exit(nullptr);
}int main()
{cout << "I am main thread" << endl;pthread_t tid;pthread_create(&tid, nullptr, run, nullptr);sleep(3);pthread_cancel(tid);void *ret = nullptr;int n = pthread_join(tid, &ret);if (n != 0){cout << "join new thread fail!!" << endl;exit(1);}if (ret == PTHREAD_CANCELED){cout << "new thread is canceled" << endl;}cout << "join new thread success!!" << endl;return 0;
}
[ckf@VM-8-3-centos lesson9_thread]$ ./mythread
I am main thread
I am new thread
I am new thread
I am new thread
new thread is canceled #新線程被撤銷了
join new thread success!!
用戶級線程
💭pthread庫的線程控制接口,都不是直接操作Linux底層的輕量級進程,而是操作用戶級線程。pthread庫將底層的輕量級進程封裝成為用戶級線程,用戶看到的便是線程而不是所謂的輕量級進程。動態庫load到進程的共享區中,因此,用戶級線程的空間也是load到進程的共享區中,線程的大部分獨立資源保存在這塊空間中,包括線程棧。
🔎線程庫是怎么管理用戶級線程的?
先描述再組織。 創建類似TCB的數據結構來描述線程,并將這些數據結構組織為一張表,如下。
-
前面使用接口獲取到的線程tid,其實就是該線程的用戶級頁表的首地址,只不過將其轉換成整型的格式。
int g_val = 100;string toHex(pthread_t tid) {char buf[64];snprintf(buf, sizeof(buf), "0x%x", tid);return string(buf); }void *run(void *arg) {cout << toHex(pthread_self()) << endl;pthread_exit(nullptr); }int main() {pthread_t t1;pthread_t t2;cout << "&g_val: " << &g_val <<endl;pthread_create(&t1, nullptr, run, nullptr);pthread_create(&t2, nullptr, run, nullptr);pthread_join(t1, nullptr);pthread_join(t2, nullptr);return 0; }
[ckf@VM-8-3-centos lesson9_thread]$ ./mythread &g_val: 0x6020cc #全局數據區 0x4b30f700 #共享區 0x4ab0e700 #共享區
-
全局變量默認是所有線程共享的,開發者需要處理多線程競爭問題。有些情況下我們需要保證一個線程獨享一份數據,其它線程無法訪問。這時候就要用到線程局部存儲。gcc/g++編譯環境中,可以用
__thread
聲明一個全局變量,從而每個線程都會獨有一個該全局變量,存儲在線程局部存儲區中。__thread int g_val = 0; //__thread修飾全局變量,可以理解為從進程的全局變量變成線程的全局變量string toHex(pthread_t tid) {char buf[64];snprintf(buf, sizeof(buf), "0x%x", tid);return string(buf); }void *run(void *arg) {cout << "g_val: " << ++g_val << " " << "&g_val: " << &g_val << endl;pthread_exit(nullptr); }int main() {pthread_t t1;pthread_t t2;pthread_t t3;pthread_create(&t1, nullptr, run, nullptr);pthread_create(&t2, nullptr, run, nullptr);pthread_create(&t3, nullptr, run, nullptr);cout << "g_val: " << ++g_val << " " << "&g_val: " << &g_val << endl;pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);return 0; }
[ckf@VM-8-3-centos lesson9_thread]$ ./mythread #使用了線程局部存儲 g_val: 1 &g_val: 0x7fcb7cfcb77c g_val: 1 &g_val: 0x7fcb7bf366fc g_val: 1 &g_val: 0x7fcb7b7356fc g_val: 1 &g_val: 0x7fcb7af346fc[ckf@VM-8-3-centos lesson9_thread]$ ./mythread #未使用線程局部存儲 g_val: 1 &g_val: 0x6021d4 g_val: 2 &g_val: 0x6021d4 g_val: 3 &g_val: 0x6021d4 g_val: 4 &g_val: 0x6021d4
-
每個線程都有一個獨立的棧結構,用于存儲運行時的臨時數據和壓入函數棧幀。注意,主線程的棧就是進程地址空間中的棧。
ENDING…