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