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