專門做蛋糕視頻的網(wǎng)站流程優(yōu)化四個方法
提示:文章寫完后,目錄可以自動生成,如何生成可參考右邊的幫助文檔
目錄
文章目錄
前言
線程的概念
線程的理解(Linux系統(tǒng)為例)
在Linux系統(tǒng)里如何保證讓正文部分的代碼可以并發(fā)的去跑呢?
為什么要有多進(jìn)程呢?
為什么要這么設(shè)計(jì)Linux"線程"?
線程的優(yōu)點(diǎn)
線程的缺點(diǎn)
線程異常
線程用途
Linux進(jìn)程 VS 線程
進(jìn)程和線程
進(jìn)程的多個線程共享
進(jìn)程和線程的關(guān)系
關(guān)于調(diào)度的問題
再次談?wù)勥M(jìn)程地址空間
多個執(zhí)行流是如何進(jìn)行代碼劃分的?如何理解?
OS如何管理內(nèi)存呢?
線程的控制
POSIX線程庫
創(chuàng)建線程
PID和LWP
Linux中有沒有真線程呢?
線程ID及進(jìn)程地址空間布局
線程終止
線程等待
分離線程
面試題
多線程創(chuàng)建
總結(jié)
前言
世上有兩種耀眼的光芒,一種是正在升起的太陽,一種是正在努力學(xué)習(xí)編程的你!一個愛學(xué)編程的人。各位看官,我衷心的希望這篇博客能對你們有所幫助,同時也希望各位看官能對我的文章給與點(diǎn)評,希望我們能夠攜手共同促進(jìn)進(jìn)步,在編程的道路上越走越遠(yuǎn)!
提示:以下是本篇文章正文內(nèi)容,下面案例可供參考
線程的概念
- 線程是進(jìn)程內(nèi)部的一個執(zhí)行分支,線程在進(jìn)程的地址空間內(nèi)運(yùn)行。
- 線程是CPU調(diào)度的基本單位,CPU在調(diào)度的時候,會有很多進(jìn)程和線程混在一起,但是CPU不管這些,在調(diào)度的時候,都是讓task_struct進(jìn)行排隊(duì)的,CPU只調(diào)度task_struct,所以說線程是CPU調(diào)度的基本單位是對的。
線程的理解(Linux系統(tǒng)為例)
- 在一個程序里的一個執(zhí)行路線就叫做線程(thread)。更準(zhǔn)確的定義是:線程是“一個進(jìn)程內(nèi)部的控制序列”
- 一切進(jìn)程至少都有一個執(zhí)行線程
- 線程在進(jìn)程內(nèi)部運(yùn)行,本質(zhì)是在進(jìn)程地址空間內(nèi)運(yùn)行
- 在Linux系統(tǒng)中,在CPU眼中,看到的PCB都要比傳統(tǒng)的進(jìn)程更加輕量化
- 透過進(jìn)程虛擬地址空間,可以看到進(jìn)程的大部分資源,將進(jìn)程資源合理分配給每個執(zhí)行流,就形成了線程執(zhí)行流
- 正文:代碼段(區(qū)),我們的代碼在進(jìn)程中,全部都是串行調(diào)用的。
- 就一個進(jìn)程,正文部分有很多對應(yīng)的函數(shù),但我們在執(zhí)行的時候,所有的函數(shù)調(diào)用都是串行調(diào)用的。
- 比如:main()函數(shù)中有a、b、c、d四個函數(shù),我們單進(jìn)程執(zhí)行main()函數(shù)時,所有的函數(shù)都是串行跑的;那么今天我們想辦法將代碼拆成兩部分,a、b函數(shù)一部分,c、d函數(shù)一部分,讓一個執(zhí)行流執(zhí)行a、b,讓另一個執(zhí)行流執(zhí)行c、d函數(shù),如果a、b和c、d函數(shù)沒有明顯的前后關(guān)系的話,分成兩個執(zhí)行流讓它能跑,那么此時我們的函數(shù)調(diào)用過程就是并行跑了。
無論是多進(jìn)程,還是多線程,它的核心思想:把串行的東西變成并行的東西。
在Linux系統(tǒng)里如何保證讓正文部分的代碼可以并發(fā)的去跑呢?
- 以前:再創(chuàng)建進(jìn)程PCB、進(jìn)程地址空間、頁表,再從磁盤中向物理內(nèi)存中加載新的程序,經(jīng)過新創(chuàng)建進(jìn)程的頁表與物理內(nèi)存建立映射關(guān)系,此時就有了獨(dú)立的代碼和數(shù)據(jù),獨(dú)立的內(nèi)核數(shù)據(jù)結(jié)構(gòu),所以這兩個進(jìn)程是獨(dú)立的。但是我們發(fā)現(xiàn)按照之前的做法,進(jìn)程創(chuàng)建的成本(時間和空間)是非常高的。
- 用戶想要的是多執(zhí)行流,所以Linux創(chuàng)建了一個線程,假設(shè)正文部分有很多的代碼,想辦法將代碼分為若干份區(qū)域,比如三份區(qū)域,進(jìn)程地址空間中的其它區(qū)域可以都看到,再創(chuàng)建一個執(zhí)行流的時候,不用創(chuàng)建地址空間和頁表,只需要在地址空間內(nèi)創(chuàng)建兩個新的task_struct,讓兩個新的task_struct指向同一塊進(jìn)程地址空間,那么它們就能看到同一份地址空間的資源,讓A進(jìn)程用第一個區(qū)域,讓B進(jìn)程用第二個區(qū)域,讓C進(jìn)程用第三個區(qū)域,那么CPU調(diào)度的時候,拿著三個task_struct,把當(dāng)前進(jìn)程的串行執(zhí)行的三份代碼,變成了并發(fā)式執(zhí)行這三份代碼了,所以我們把這種在地址空間內(nèi)創(chuàng)建的"進(jìn)程",把它叫做線程。
進(jìn)程地址空間上布滿了虛擬地址,進(jìn)程地址空間以及上面的虛擬地址的本質(zhì)是一種資源。
我們之前說的,代碼可以并行或并發(fā)的去跑,比如:父子進(jìn)程的代碼是共享的,數(shù)據(jù)寫實(shí)拷貝各自一份,所以可以讓父子執(zhí)行不同的代碼塊,這樣就可以將代碼塊進(jìn)行兩個各自調(diào)度運(yùn)行了。
為什么要有多進(jìn)程呢?
目標(biāo)不是為了多進(jìn)程,是為了多執(zhí)行流并發(fā)執(zhí)行,為了讓多個進(jìn)程之間可以并發(fā)的去跑相同或不同的代碼。
為什么要這么設(shè)計(jì)Linux"線程"?
線程跟進(jìn)程一樣,也是要被調(diào)度的。
線程在一個進(jìn)程內(nèi)部,就意味著一個進(jìn)程內(nèi)部可能會存在很多個線程。
如果我們要設(shè)計(jì)線程,OS也要對線程進(jìn)行管理!先描述,再組織。描述線程:線程控制塊(struct TCB),要保證線程被OS管理,比如用鏈表將線程管理,還要保證進(jìn)程PCB和這些線程進(jìn)行關(guān)聯(lián),PCB中的對應(yīng)的指針指向?qū)?yīng)的線程,但是這樣是非常復(fù)雜的。
- 管理線程的策略和進(jìn)程是非常像的,OS要求我們對應(yīng)的線程在進(jìn)程內(nèi)運(yùn)行,是進(jìn)程內(nèi)的執(zhí)行分支,只要符合這個特點(diǎn),就都是線程,并不一定必須上面的實(shí)現(xiàn)。管理進(jìn)程已經(jīng)設(shè)計(jì)數(shù)據(jù)結(jié)構(gòu),設(shè)計(jì)調(diào)度算法,還寫了創(chuàng)建、等待、終止等各種接口,那么可以把進(jìn)程的數(shù)據(jù)結(jié)構(gòu)和調(diào)度算法等代碼復(fù)用起來。
- Linux的設(shè)計(jì)者認(rèn)為,進(jìn)程和線程都是執(zhí)行流,具有極度的相似性,沒有必要單獨(dú)設(shè)計(jì)數(shù)據(jù)結(jié)構(gòu)和調(diào)度算法,直接復(fù)用代碼。
- 使用進(jìn)程來模擬線程。
- Windows是單獨(dú)的設(shè)計(jì)了線程模塊。Linux用的是復(fù)用進(jìn)程的代碼來設(shè)計(jì)的。
線程的優(yōu)點(diǎn)
- 創(chuàng)建一個新線程的代價(jià)要比創(chuàng)建一個新進(jìn)程小得多
- 與進(jìn)程之間的切換相比,線程之間的切換需要操作系統(tǒng)做的工作要少很多
- 線程占用的資源要比進(jìn)程少很多
- 能充分利用多處理器的可并行數(shù)量
- 在等待慢速I/O操作結(jié)束的同時,程序可執(zhí)行其他的計(jì)算任務(wù)
- 計(jì)算密集型應(yīng)用,為了能在多處理器系統(tǒng)上運(yùn)行,將計(jì)算分解到多個線程中實(shí)現(xiàn)
- I/O密集型應(yīng)用,為了提高性能,將I/O操作重疊。線程可以同時等待不同的I/O操作
線程的缺點(diǎn)
性能損失
- 一個很少被外部事件阻塞的計(jì)算密集型線程往往無法與共它線程共享同一個處理器。如果計(jì)算密集型 線程的數(shù)量比可用的處理器多,那么可能會有較大的性能損失,這里的性能損失指的是增加了額外的 同步和調(diào)度開銷,而可用的資源不變。
健壯性降低
- 編寫多線程需要更全面更深入的考慮,在一個多線程程序里,因時間分配上的細(xì)微偏差或者因共享了 不該共享的變量而造成不良影響的可能性是很大的,換句話說線程之間是缺乏保護(hù)的。
缺乏訪問控制
- 進(jìn)程是訪問控制的基本粒度,在一個線程中調(diào)用某些OS函數(shù)會對整個進(jìn)程造成影響。
編程難度提高
- 編寫與調(diào)試一個多線程程序比單線程程序困難得多
線程異常
- 單個線程如果出現(xiàn)除零,野指針問題導(dǎo)致線程崩潰,進(jìn)程也會隨著崩潰
- 線程是進(jìn)程的執(zhí)行分支,線程出異常,就類似進(jìn)程出異常,進(jìn)而觸發(fā)信號機(jī)制,終止進(jìn)程,進(jìn)程終止,該 進(jìn)程內(nèi)的所有線程也就隨即退出
線程用途
- 合理的使用多線程,能提高CPU密集型程序的執(zhí)行效率
- 合理的使用多線程,能提高IO密集型程序的用戶體驗(yàn)(如生活中我們一邊寫代碼一邊下載開發(fā)工具,就是 多線程運(yùn)行的一種表現(xiàn))
Linux進(jìn)程 VS 線程
進(jìn)程和線程
- 以前的進(jìn)程:一個內(nèi)部只有一個線程的進(jìn)程。
- 今天的進(jìn)程:一個內(nèi)部至少有一個線程的進(jìn)程。我們以前的講的進(jìn)程是今天講的進(jìn)程的一種特殊情況。
什么是進(jìn)程呢?
- 內(nèi)核的數(shù)據(jù)結(jié)構(gòu)+進(jìn)程的代碼和數(shù)據(jù)(也就是一個或者多個執(zhí)行流、進(jìn)程地址空間、頁表和進(jìn)程的代碼和數(shù)據(jù))
- 線程(task_struct)叫做進(jìn)程內(nèi)部的一個執(zhí)行分支。
- 線程是調(diào)度的基本單位。
- 進(jìn)程的內(nèi)核角度:承擔(dān)分配系統(tǒng)資源的基本實(shí)體。
- 不要站在調(diào)度角度理解進(jìn)程,而應(yīng)該站在資源角度理解進(jìn)程。
線程共享進(jìn)程數(shù)據(jù),但也擁有自己的一部分?jǐn)?shù)據(jù):
- 線程ID
- 一組寄存器
- 棧
- errno
- 信號屏蔽字
- 調(diào)度優(yōu)先級
講一個故事:
- 我們的社會就是一個大的系統(tǒng),在社會中承擔(dān)分配社會資源(汽車、彩電等)的基本實(shí)體是家庭,家庭中的每一個人都是一個執(zhí)行流,各自都做著不同的事情,但每一個人都會互相協(xié)作起來,完成一個公共的事情,把日子過好。家庭中的每一個人就是線程,家庭就是一個進(jìn)程。
進(jìn)程的多個線程共享
共享同一地址空間,因此代碼段(Text Segment)、數(shù)據(jù)段(Data Segment)都是共享的:
- 如果定義一個函數(shù),在各線程中都可以調(diào)用;
- 如果定義一個全局變量,在各線程中都可以訪問到;
除此之外,各線程還共享以下進(jìn)程資源和環(huán)境:
- 文件描述符表
- 每種信號的處理方式(SIG_ IGN、SIG_ DFL或者自定義的信號處理函數(shù))
- 當(dāng)前工作目錄
- 用戶id和組id
進(jìn)程和線程的關(guān)系
關(guān)于調(diào)度的問題
CPU在選擇執(zhí)行流去調(diào)度的時候,用不用區(qū)分一個進(jìn)程內(nèi)部唯一的執(zhí)行流呢?還是一個進(jìn)程內(nèi)部某一個執(zhí)行流呢?
不需要管。因?yàn)榫€程也有PCB、進(jìn)程地址空間、頁表、進(jìn)程代碼和數(shù)據(jù),與進(jìn)程一致,都是執(zhí)行流,不需要區(qū)分。
- CPU調(diào)度的執(zhí)行流:線程 <= 執(zhí)行流 <= 進(jìn)程
- Linux是用進(jìn)程模擬的線程,所以Linux系統(tǒng)里嚴(yán)格意義上講,不存在物理上的真正的線程,因?yàn)闆]有單獨(dú)為線程創(chuàng)建struct TCB。
- Linux中,所有的調(diào)度執(zhí)行流都叫做:輕量級進(jìn)程。
再次談?wù)勥M(jìn)程地址空間
多個執(zhí)行流是如何進(jìn)行代碼劃分的?如何理解?
- 如果進(jìn)程執(zhí)行的代碼也是一個操作系統(tǒng),這就相當(dāng)于使用了進(jìn)程的殼子,完成了一種內(nèi)核級虛擬機(jī)的技術(shù)。
- OS要不要管理內(nèi)存呢?大部分OS中的內(nèi)存在系統(tǒng)角度是以4KB為單位的內(nèi)存塊。
- 一個可執(zhí)行程序加載是以平坦模式把整個代碼進(jìn)行編址的,對應(yīng)的可執(zhí)行程序也在內(nèi)部按地址劃分成4KB的數(shù)據(jù)塊。
- 可執(zhí)行程序里的二進(jìn)制代碼只要寫到文件系統(tǒng)里,天然就是4KB的。
- 從此磁盤和物理內(nèi)存進(jìn)行交互時,就以數(shù)據(jù)塊為單位,這就叫做4KB數(shù)據(jù)塊。
- 內(nèi)存中的4KB數(shù)據(jù)塊叫做空間;磁盤中文件的4KB數(shù)據(jù)塊叫做內(nèi)容;所謂的加載,就是將內(nèi)容放入空間當(dāng)中,在OS的術(shù)語里,一般我們把4KB的空間或內(nèi)容叫做頁框或者頁幀。
OS如何管理內(nèi)存呢?
先描述,再組織!用struct page結(jié)構(gòu)體來描述內(nèi)存中的4KB數(shù)據(jù)塊,假設(shè)有萬4GB,4GB內(nèi)存中有100多個4KB的數(shù)據(jù)塊,用struct page mem[100多萬]數(shù)組來組織,天然的每一個4KB就有了它的編號(物理地址),編號就是下標(biāo),對內(nèi)存進(jìn)行管理,就是對該數(shù)組的增刪查改。未來加載程序時,有多少個4KB的數(shù)據(jù)塊要加載,我們就在內(nèi)存當(dāng)中申請多少個數(shù)據(jù)塊,將程序數(shù)據(jù)塊中的內(nèi)容加載到數(shù)組下標(biāo)中的數(shù)據(jù)塊空間中。
- OS進(jìn)行內(nèi)存管理的基本單位是4KB。
- 所以以前講過的父子進(jìn)程代碼共享,數(shù)據(jù)各自私有一份,內(nèi)存塊中保存的代碼,它里面配置的引用計(jì)數(shù)就是2(父子進(jìn)程都指向它),所以子進(jìn)程退出了,不影響父進(jìn)程,引用計(jì)數(shù)--;寫實(shí)拷貝是以4KB為單位進(jìn)行的,不是只以變量為單位進(jìn)行的,像new、malloc申請對象的時候,在OS也是以4KB為單位申請空間的。
- 可執(zhí)行程序沒有被加載之前,就已經(jīng)有虛擬地址了,加載到物理內(nèi)存之后,程序內(nèi)部用虛擬地址,定位我們的程序用物理地址。
以32位平臺下為例:
將虛擬地址轉(zhuǎn)換成物理地址:
- 虛擬地址(32byte)不是鐵板一塊,虛擬地址被OS看成10、10、12三個子區(qū)域,
- OS在進(jìn)程創(chuàng)建、加載時,根本就不需要搞一個大頁表,而只需要,從左往右數(shù)的前10個比特位(第一個子區(qū)域),這10個比特位從全0到全1的取值范圍為:[0,1023]/[0~2^10-1];第二個子區(qū)域的范圍:[0,1023];
- 在剛開始創(chuàng)建進(jìn)程的時候,必須給進(jìn)程創(chuàng)建頁表,這句話沒錯,但剛開始創(chuàng)建的不是完整的頁表,我們只需要創(chuàng)建第一個子區(qū)域的頁表,頁表中有1024個項(xiàng),查頁表時,需要先拿虛擬地址的前10個比特位檢索這張表,這張表叫做頁目錄。
- 第二個子區(qū)域也要創(chuàng)建一張或者多張表,這些表才是頁表,我們把頁表和頁目錄里面的條目叫做頁表項(xiàng),頁目錄里面保存的是二級頁表的地址,在查頁表時,先拿虛擬地址的前10個比特位做第一張表的索引,再拿虛擬地址的中間10個byte來查頁表,OS當(dāng)中最多會存在1024張頁表(不可能的);頁表中存放的是物理內(nèi)存中每一個4KB數(shù)據(jù)塊的起始地址,假設(shè)訪問一個頁表中存放4KB數(shù)據(jù)塊的起始地址0x1234,訪問的并不是4KB的數(shù)據(jù)塊,而是訪問的是4KB里面的某一個區(qū)域或字節(jié),因?yàn)榫€性地址,地址空間的基本單位是1字節(jié)的,可以0x1234 + 虛擬地址后的12位(第三個子區(qū)域)對應(yīng)的數(shù)據(jù) == 訪問到4KB數(shù)據(jù)塊的全部內(nèi)容。
- 為什么是12位呢?因?yàn)?^12就是4KB。虛擬地址的后12位,我們稱之為頁內(nèi)偏移。所以我們查頁表只是用虛擬地址的前20位btye。頁表里面保存的是頁框的物理地址。
- 頁目錄占4KB空間,頁表最多占4MB空間,所以整個頁表內(nèi)容,我們用4MB就能表示完了。
- 頁表中也可以加一些標(biāo)志位,表示對應(yīng)的數(shù)據(jù)塊是內(nèi)核用的,還是用戶用的,還有權(quán)限等。
結(jié)論:給不同的線程分配不同的區(qū)域,本質(zhì)就是給讓不同的線程,各自看到全部頁表的子集。就是讓不同的線程看到不同的頁表。
線程的控制
POSIX線程庫
- 與線程有關(guān)的函數(shù)構(gòu)成了一個完整的系列,絕大多數(shù)函數(shù)的名字都是以“pthread_”打頭的
- 要使用這些函數(shù)庫,要通過引入頭文<pthread.h>
- 鏈接這些線程函數(shù)庫時要使用編譯器命令的“-lpthread”選項(xiàng)
創(chuàng)建線程
功能:創(chuàng)建一個新的線程
原型
int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void*(*start_routine)(void*), void* arg);
參數(shù)參數(shù)1:輸出型參數(shù),創(chuàng)建成功會帶出新線程id;參數(shù)2: 設(shè)置線程的屬性,attr為NULL表示使用默認(rèn)屬性參數(shù)3:返回值為void* ,參數(shù)為void* 的函數(shù)指針,讓新線程來執(zhí)行這個函數(shù)方法參數(shù)4:傳遞給線程函數(shù)的參數(shù),參數(shù)會傳遞到參數(shù)3中去
返回值:成功返回0;失敗返回錯誤碼
內(nèi)部創(chuàng)建線程之后,將來會有兩個執(zhí)行流,一個是主線程,一個是新創(chuàng)建的線程,新創(chuàng)建的線程會回調(diào)式的調(diào)用參數(shù)3(函數(shù)指針)。
錯誤檢查:
- 傳統(tǒng)的一些函數(shù)是,成功返回0,失敗返回-1,并且對全局變量errno賦值以指示錯誤。
- pthreads函數(shù)出錯時不會設(shè)置全局變量errno(而大部分其他POSIX函數(shù)會這樣做)。而是將錯誤代碼通過返回值返回
- pthreads同樣也提供了線程內(nèi)的errno變量,以支持其它使用errno的代碼。對于pthreads函數(shù)的錯誤, 建議通過返回值業(yè)判定,因?yàn)樽x取返回值要比讀取線程內(nèi)的errno變量的開銷更小
#include <iostream>
#include <pthread.h> // 在Liinux中使用線程,要包含頭文件
#include <unistd.h>
#include <sys/types.h>// 新線程
void* newthreadrun(void* args)
{while (true){std::cout << "I am new thread, pid: " << getpid() << std::endl;sleep(1);}
}int main()
{pthread_t tid;// 線程id// 創(chuàng)建新線程pthread_create(&tid, nullptr, newthreadrun, nullptr);while (true){std::cout << "I am main thread, pid: " << getpid() << std::endl;sleep(1);}
}
PID和LWP
- ps -aL中的L:能查看真實(shí)存在的多線程(輕量級進(jìn)程)
- OS在進(jìn)行調(diào)度的時候,用輕量級進(jìn)程(LWP)的id來進(jìn)行調(diào)度。
- 單進(jìn)程和多進(jìn)程在調(diào)度的時候,也在看LWP,因?yàn)楫?dāng)每一個進(jìn)程內(nèi)部都只有一個執(zhí)行流時,LWP == PID,此時調(diào)用那個都是一樣的。
- LWP和PID說明PCB里面每一個都包含PID,PID表示PCB屬于哪一個進(jìn)程;LWP表明PCB是進(jìn)程中的那個執(zhí)行流。
- getpid()獲得進(jìn)程的pid,而不是獲取的是LWP的id,OS沒有直接提供獲取LWP的id的系統(tǒng)調(diào)用。
- 函數(shù)編譯完成,是若干行代碼塊,每一行代碼都有地址(虛擬地址/磁盤上-邏輯地址),函數(shù)名是該代碼塊的入口地址。所有的函數(shù),都要按照地址空間統(tǒng)一編址。
- ps -aL:查看的輕量級進(jìn)程,所看到的LWP是線程的id,與pthread_create()函數(shù)中參數(shù)1所得到的新線程的id,兩者的表現(xiàn)形式是不同的,因?yàn)長WP是在內(nèi)核當(dāng)中來標(biāo)識一個執(zhí)行流的唯一性的,所以只在OS內(nèi)使用,但是創(chuàng)建線程pthread_create(),用的線程是屬于線程庫,所以pthread_create()函數(shù)的參數(shù)1得到的id是線程庫來維護(hù)的。這兩個id是一對一的,一個是在用戶層的,一個是在內(nèi)核層的。
Linux中有沒有真線程呢?
- 沒有。Linux中只有輕量級進(jìn)程。
- 為了保證自己的純潔性和簡潔性,所以Linux系統(tǒng),不會有線程相關(guān)的系統(tǒng)調(diào)用,只有輕量級進(jìn)程的系統(tǒng)調(diào)用。
- 為了讓用戶選擇Linux系統(tǒng),為了讓用戶能正常的使用對應(yīng)的線程功能,Linux設(shè)計(jì)者在用戶和Linux系統(tǒng)之間設(shè)計(jì)了一個中間的軟件層,軟件層叫做pthread庫(原生線程庫),任何的Linux內(nèi)核里面,你在安裝的時候,pthread庫必須在Linux系統(tǒng)里自帶,在系統(tǒng)里默認(rèn)就裝好了,pthread庫的作用是將輕量級進(jìn)程的系統(tǒng)調(diào)用進(jìn)行封裝,轉(zhuǎn)成線程相關(guān)的接口語義提供給用戶,底層其實(shí)還是輕量級進(jìn)程。
用戶知道"輕量級進(jìn)程"這個概念嗎?
沒有。用戶只認(rèn)進(jìn)程和線程。其實(shí)輕量級進(jìn)程就是線程。
pthread庫不屬于OS內(nèi)核,只要是庫就是在用戶級實(shí)現(xiàn)的,所以Linux的線程也別叫做用戶級線程。所以編寫多線程時,都必須要鏈接上這個pthread庫:-lpthread
testthread:testThread.ccg++ - o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm - f testthread
線程ID及進(jìn)程地址空間布局
- pthread_ create函數(shù)會產(chǎn)生一個線程ID,存放在第一個參數(shù)指向的地址中。該線程ID和前面說的線程ID不是一回事。
- 前面講的線程ID屬于進(jìn)程調(diào)度的范疇。因?yàn)榫€程是輕量級進(jìn)程,是操作系統(tǒng)調(diào)度器的最小單位,所以需要一個數(shù)值來唯一表示該線程。
- pthread_ create函數(shù)第一個參數(shù)指向一個虛擬內(nèi)存單元,該內(nèi)存單元的地址即為新創(chuàng)建線程的線程ID, 屬于NPTL線程庫的范疇。線程庫的后續(xù)操作,就是根據(jù)該線程ID來操作線程的。
- 線程庫NPTL提供了pthread_ self函數(shù),可以獲得線程自身的ID:
man pthread_self // 那個線程調(diào)用pthread_self()函數(shù),就獲取那個線程的id
pthread_t pthread_self(void);
pthread_t 到底是什么類型呢?取決于實(shí)現(xiàn)。對于Linux目前實(shí)現(xiàn)的NPTL實(shí)現(xiàn)而言,pthread_t類型的線程ID,本質(zhì) 就是一個進(jìn)程地址空間上的一個地址。
線程終止
如果需要只終止某個線程而不終止整個進(jìn)程,可以有三種方法:
- 從線程函數(shù)return。這種方法對主線程不適用,從main函數(shù)return相當(dāng)于調(diào)用exit。
- 線程可以調(diào)用pthread_ exit終止自己。
- 一個線程可以調(diào)用pthread_ cancel終止同一進(jìn)程中的另一個線程。
功能:線程終止
原型
? ? void pthread_exit(void* value_ptr);// 那個線程調(diào)用該函數(shù),就終止那個線程
參數(shù)
? ? value_ptr: value_ptr不要指向一個局部變量。
返回值:無返回值,跟進(jìn)程一樣,線程結(jié)束的時候無法返回到它的調(diào)用者(自身)
需要注意,pthread_exit或者return返回的指針?biāo)赶虻膬?nèi)存單元必須是全局的或者是用malloc分配的,不能在線程函 數(shù)的棧上分配,因?yàn)楫?dāng)其它線程得到這個返回指針時線程函數(shù)已經(jīng)退出了。
功能:取消一個執(zhí)行中的線程
原型
? ? int pthread_cancel(pthread_t thread);
參數(shù)
? ? thread : 線程ID
返回值:成功返回0;失敗返回錯誤碼
線程等待
為什么需要線程等待?
- 已經(jīng)退出的線程,其空間沒有被釋放,仍然在進(jìn)程的地址空間內(nèi)。
- 創(chuàng)建新的線程不會復(fù)用剛才退出線程的地址空間。
man pthread_join ?
int pthread_join(pthread_t thread, void **value_ptr); 等待一個已經(jīng)結(jié)束的線程
- 參數(shù)1:等待指定的一個線程,如果該線程沒有退出,會阻塞式等待,若該線程退出了,則返回等待的結(jié)果;
- 參數(shù)2:輸出型參數(shù),拿到的是新線程對應(yīng)的返回值
返回值:成功返回0;失敗返回錯誤碼
調(diào)用該函數(shù)的線程將掛起等待,直到id為thread的線程終止。thread線程以不同的方法終止,通過pthread_join得到的終止?fàn)顟B(tài)是不同的,總結(jié)如下:
- 如果thread線程通過return返回,value_ ptr所指向的單元里存放的是thread線程函數(shù)的返回值。
- 如果thread線程被別的線程調(diào)用pthread_ cancel異常終掉,value_ ptr所指向的單元里存放的是常數(shù) PTHREAD_ CANCELED,就是-1。
- 如果thread線程是自己調(diào)用pthread_exit終止的,value_ptr所指向的單元存放的是傳給pthread_exit的參數(shù)。
- 如果對thread線程的終止?fàn)顟B(tài)不感興趣,可以傳NULL給value_ ptr參數(shù)。
分離線程
- 默認(rèn)情況下,新創(chuàng)建的線程是joinable的,線程退出后,需要對其進(jìn)行pthread_join操作,否則無法釋放資源,從而造成系統(tǒng)泄漏。
- 如果不關(guān)心線程的返回值,join是一種負(fù)擔(dān),這個時候,我們可以告訴系統(tǒng),當(dāng)線程退出時,自動釋放線程資源。
把一個線程設(shè)置成分離:
man pthread_detach ?
int pthread_detach(pthread_t thread);
參數(shù):要分離哪一個線程的id
可以是線程組內(nèi)其他線程對目標(biāo)線程進(jìn)行分離,也可以是線程自己分離:
pthread_detach(pthread_self());
joinable和分離是沖突的,一個線程不能既是joinable又是分離的。
// 同一個進(jìn)程內(nèi)的線程,大部分資源都是共享的. 地址空間是共享的!
// 比如:初始化和未初始化區(qū)域、還有正文部分沒有被線程分走的其它代碼也是這個進(jìn)程所有的線程共享的。
int g_val = 100;// 將新的線程id轉(zhuǎn)換成16進(jìn)制的形式
std::string ToHex(pthread_t tid)
{char id[64];snprintf(id, sizeof(id), "0x%lx", tid);return id;
}// 線程退出// 1. 代碼跑完,結(jié)果對// 2. 代碼跑完,結(jié)果不對// 3. 出異常了 --- 重點(diǎn) --- 多線程中,任何一個線程出現(xiàn)異常(div 0, 野指針), 都會導(dǎo)致整個進(jìn)程退出! // ---- 多線程代碼往往健壯性不好void *threadrun(void *args){// 將函數(shù)的參數(shù)傳遞過來,字符串的地址來用于初始化新線程的名字std::string threadname = (char*)args;int cnt = 5;while (cnt){printf("new thread, g_val: %d, &g_val: %p\n", g_val, &g_val);// std::cout << threadname << " is running: " << cnt << ", pid: " << getpid()// << " mythread id: " << ToHex(pthread_self())// << "g_val: "<< g_val << " &g_val: " << &g_val << std::endl;g_val++;sleep(1);// int *p = nullptr;// *p = 100; // 故意一個野指針cnt--;}// 1. 線程函數(shù)結(jié)束 法1:(return)// 2. 法2:pthread_exit()// pthread_exit((void*)123);// 終止新線程// exit(10); // 不能用exit終止線程,因?yàn)樗墙K止進(jìn)程的.return (void*)123; // warning}// 主線程退出 == 進(jìn)程退出 == 所有線程都要退出(資源都被釋放)// 1. 往往我們需要main thread最后結(jié)束// 2. 線程也要被"wait", 要不然會產(chǎn)生類似進(jìn)程哪里的內(nèi)存泄漏的問題(線程是需要被等待的)int main(){// 1. idpthread_t tid;// pthread_t就是一個無符號的長整型pthread_create(&tid, nullptr, threadrun, (void*)"thread-1");// 參數(shù)1:輸出型參數(shù),得到的是新線程的id // 法3: // 在主線程中,你保證新線程已經(jīng)啟動// sleep(2);// pthread_cancel(tid);// 取消tid線程,那么pthread_join()函數(shù)拿到的就是線程的退出碼-1,-1就是宏,-1表示這個線程是被取消的// 2. 新和主兩個線程,誰先運(yùn)行呢?不確定,由調(diào)度器決定int cnt = 10;while (true){std::cout << "main thread is running: " << cnt << ", pid: "<< getpid() << " new thread id: " << ToHex(tid) << " "<< " main thread id: " << ToHex(pthread_self())<< "g_val: "<< g_val << " &g_val: " << &g_val << std::endl;printf("main thread, g_val: %d, &g_val: %p\n", g_val, &g_val);sleep(1);cnt--;}// 如果主線程比新線程提前退出了呢?void* ret = nullptr;// void:是不能定義變量的;void*:能定義變量,指針變量是已經(jīng)開辟了空間的// PTHREAD_CANCELED; // (void*)-1// 我們怎么沒有像進(jìn)程一樣獲取線程退出的退出信號呢?只有你手動寫的退出碼// 所等的線程一旦產(chǎn)生信號了,線程所在的進(jìn)程就被干掉了,所以pthread_join沒有機(jī)會獲得信號。// 所以pthread_join()函數(shù)不考慮線程異常情況!int n = pthread_join(tid, &ret); std::cout << "main thread quit, n=" << n << " main thread get a ret: " << (long long)ret << std::endl;return 0;}
新線程所產(chǎn)生的異常由父進(jìn)程去考慮。
std::string ToHex(pthread_t tid){char id[64];snprintf(id, sizeof(id), "0x%lx", tid);return id;}__thread uint64_t starttime = 100;// __thread int tid = 0;// 全局變量g_val屬于已初始化區(qū)域,是所有線程共享的資源// __thread:讓這個進(jìn)程中所有的線程都私有一份g_val全局變量// __thread:編譯器在編譯時將g_val變量拆分出來,放到了每個線程的局部存儲空間內(nèi)int g_val = 100;// 主線程一直在等待新線程,在等待期間,不會創(chuàng)造價(jià)值,所以有類似于非阻塞等待:// 線程是可以分離的: 默認(rèn)線程是joinable(需要被等待)的。// 如果我們main thread不關(guān)心新線程的執(zhí)行信息,我們可以將新線程設(shè)置為分離狀態(tài):// 你是如何理解線程分離的呢?底層依舊屬于同一個進(jìn)程!只是不需要等待了// 一般都希望mainthread 是最后一個退出的,無論是否是join、detachvoid *threadrun1(void *args){starttime = time(nullptr);// pthread_detach(pthread_self());// 該線程自己分離自己,則主線程不會再等待新線程std::string name = static_cast<const char *>(args);while(true){sleep(1);printf("%s, g_val: %lu, &g_val: %p\n", name.c_str(), starttime, &starttime);}return nullptr;}void *threadrun2(void *args){sleep(5);starttime = time(nullptr);// pthread_detach(pthread_self());std::string name = static_cast<const char *>(args);while(true){printf("%s, g_val: %lu, &g_val: %p\n", name.c_str(), starttime, &starttime);sleep(1);}return nullptr;}int main(){pthread_t tid1;pthread_t tid2;pthread_create(&tid1, nullptr, threadrun1, (void *)"thread 1");pthread_create(&tid2, nullptr, threadrun2, (void *)"thread 2");pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);// pthread_detach(tid);// 可以由主線程來進(jìn)行使新線程進(jìn)行分離// std::cout << "new tid: " << tid << ", hex tid: " << ToHex(tid) << std::endl;// std::cout << "main tid: " << pthread_self() << ", hex tid: " << ToHex(pthread_self()) << std::endl;// int cnt = 5;// while (true)// {// if (!(cnt--))// break;// std::cout << "I am a main thread ..." << getpid() << std::endl;// sleep(1);// }// std::cout << "main thread wait block" << std::endl;// 主線程要等待新線程,否則會出現(xiàn)類似于僵尸進(jìn)程的問題// 若是新線程是分離的狀態(tài),等待的話,會出錯返回int n = pthread_join(tid, nullptr);std::cout << "main thread wait return: " << n << ": " << strerror(n) << std::endl;return 0;}
面試題
與進(jìn)程之間的切換相比,線程之間的切換需要操作系統(tǒng)做的工作要少很多
- 當(dāng)一個進(jìn)程被CPU調(diào)度時,CPU內(nèi)部一定存在非常多的寄存器,寄存器當(dāng)中保存的都是當(dāng)前進(jìn)程運(yùn)行的上下文數(shù)據(jù),比如:把CPU內(nèi)部寄存器的值保存到PCB當(dāng)中,下次再調(diào)用時,再恢復(fù)過來;
- 寄存器CR3保存了頁表的起始地址,CPU要切換一個進(jìn)程的話,我們只需要把PCB、進(jìn)程地址空間、頁表切換就行了;
- 今天再加了一個線程,線程也是調(diào)度的實(shí)體,當(dāng)切換線程時,CPU內(nèi)部的寄存器中也會有各種的數(shù)據(jù),那么線程切換也要進(jìn)行上下文的保護(hù),將數(shù)據(jù)保存到線程PCB當(dāng)中,需要再恢復(fù)出來,線程切換時,進(jìn)程地址空間和頁表不用換;
- 進(jìn)程切換的時候,進(jìn)程地址空間和頁表要切換;
- PCB指向進(jìn)程地址空間,只要PCB切換了,對應(yīng)的進(jìn)程地址空間也就切換了,切換一個頁表就是切換了CPU中存儲頁表地址的寄存器CR3;
- 兩者比較一下:線程切換,不切換頁表,進(jìn)程切換,切換一下頁表就可以了;
- 線程切換需要保存的上下文數(shù)據(jù),只比進(jìn)程少一點(diǎn)。(頁表不需要切換 == 寄存器CR3不需要改變)
在CPU內(nèi)部,每一次我們讀取當(dāng)前進(jìn)程的代碼和數(shù)據(jù)時,CPU上硬件上有一個cache,cache是一個集成在CPU內(nèi)部的,一段比寄存器容量大的多的一段緩存區(qū),當(dāng)CPU將虛擬地址轉(zhuǎn)物理地址,進(jìn)行尋址的時候,找到物理內(nèi)存中的代碼,只找到了一行代碼,但是下一次大概率還需要這一行代碼的下一行代碼,所以會將這塊相關(guān)的代碼全部搬到CPU內(nèi)部的cache中緩存起來,從此CPU訪問代碼數(shù)據(jù)的時候,不用從內(nèi)存中讀取了,而直接從CPU中較近的cache中讀取,從而大大提高CPU尋址的效率。
進(jìn)程間切換時,假設(shè)A進(jìn)程被切換下去,那么CPU內(nèi)部cache中的數(shù)據(jù)就被清空,由新切換上來的B進(jìn)程來重新填充cache中的代碼和數(shù)據(jù),這個過程很漫長,因此進(jìn)程間的切換,成本很高。對于線程切換來說,因?yàn)檫M(jìn)程地址空間、頁表、進(jìn)程的代碼和數(shù)據(jù)都是共享的,所以CPU中cache的緩存區(qū)中的數(shù)據(jù)不需要被丟棄,所以線程切換的成本要比進(jìn)程要低。
一組寄存器:
- 每個線程都是獨(dú)立的,被單獨(dú)調(diào)度的執(zhí)行流,每個線程都要有一組自己獨(dú)立的上下文數(shù)據(jù)。
線程都有自己的臨時變量,在C語言中在函數(shù)中的臨時變量都是在棧區(qū)上保存的,比如:主線程要形成自己的臨時變量,新線程也要形成自己的臨時變量,函數(shù)調(diào)用要壓棧和出棧,如果兩個線程使用的是同一個進(jìn)程地址空間上的棧區(qū),兩個都在訪問這個棧區(qū),如果一個棧區(qū)被多個線程共享的話,每個線程都要向棧區(qū)中壓棧入自己的臨時數(shù)據(jù),那么在棧中壓入的臨時變量,無法分清是那個線程的,所以庫在設(shè)計(jì)的時候,都必須保證給每個線程都要有自己獨(dú)立的用戶棧。每個線程都有自己獨(dú)立的棧結(jié)構(gòu)。
哪些屬于線程私有的?
- 線程的硬件上下文(CPU寄存器的值)(調(diào)度)
- 線程的獨(dú)立棧結(jié)構(gòu)(常規(guī)運(yùn)行)
線程共享:
- 代碼和全局?jǐn)?shù)據(jù);
- 進(jìn)程的文件描述符表
一個線程出問題,導(dǎo)致其它線程也出問題,導(dǎo)致整個進(jìn)程退出---線程安全問題。
多線程中,公共函數(shù)如果被多個線程同時進(jìn)入---該函數(shù)被重入了。
多線程創(chuàng)建
#include <iostream>
#include <string>
#include <vector>
#include <cstdio>
#include <unistd.h>
#include <cstdlib>
#include <pthread.h> // 原生線程庫的頭文件const int threadnum = 5;class Task
{
public:Task(){}void SetData(int x, int y){datax = x;datay = y;}// 執(zhí)行的任務(wù)int Excute(){return datax + datay;}~Task(){}
private:int datax;int datay;
};class ThreadData : public Task
{
public:ThreadData(int x, int y, const std::string& threadname) :_threadname(threadname){_t.SetData(x, y);}std::string threadname(){return _threadname;}int run(){return _t.Excute();}
private:std::string _threadname;Task _t;
};
// 結(jié)果
class Result
{
public:Result() {}~Result() {}void SetResult(int result, const std::string& threadname){_result = result;_threadname = threadname;}void Print(){std::cout << _threadname << " : " << _result << std::endl;}
private:int _result;std::string _threadname;
};// 每個線程都會執(zhí)行這個函數(shù)
void* handlerTask(void* args)
{ThreadData* td = static_cast<ThreadData*>(args);std::string name = td->threadname();Result* res = new Result();int result = td->run();res->SetResult(result, name);std::cout << name << "run result : " << result << std::endl;delete td;sleep(2);return res;// 這個函數(shù)沒有使用全局變量,在函數(shù)中定義的threadname變量在自己的獨(dú)立棧上,所以多個線程并不影響// 雖然該函數(shù)重入了,但是函數(shù)并不會出問題// // std::string threadname =static_cast<char*>(args);// const char *threadname = static_cast<char *>(args);// while (true)// {// std::cout << "I am " << threadname << std::endl;// sleep(2);// }// 雖然對于線程來說堆空間是共享的,但是每個線程都只能拿到自己堆空間的起始地址,其它線程的堆空間看不到// delete []threadname;// return nullptr;
}
// 1. 多線程創(chuàng)建
// 2. 線程傳參和返回值,我們可以傳遞基本信息,也可以傳遞其他對象(包括你自己定義的!)
// 3. C++11也帶了多線程,和我們今天的是什么關(guān)系???
int main()
{std::vector<pthread_t> threads;// 創(chuàng)建5個線程for (int i = 0; i < threadnum; i++){char threadname[64];// 第二次循環(huán)時,第一次循環(huán)時的緩沖區(qū)中的數(shù)據(jù)就被釋放掉或者被后來的數(shù)據(jù)覆蓋snprintf(threadname, 64, "Thread-%d", i + 1);// 將線程名為參數(shù)傳遞給線程函數(shù)// 我們不能讓每一個線程的threadname的變量都指向同一塊緩沖區(qū),我們要給每一個線程申請一個屬于自己的空間ThreadData* td = new ThreadData(10, 20, threadname);pthread_t tid;pthread_create(&tid, nullptr, handlerTask, td);threads.push_back(tid);// 將線程id保存到vector中}std::vector<Result*> result_set;// 結(jié)果void* ret = nullptr;// 循環(huán)等待線程for (auto& tid : threads){pthread_join(tid, &ret);result_set.push_back((Result*)ret);}for (auto& res : result_set){res->Print();delete res;}
}
新線程處于分離狀態(tài),新線程無線循環(huán)的跑下去,主線程5秒之后,就退出,會發(fā)生什么事情呢?
- 5秒之后,主線程和新線程都會退出。因?yàn)橹骶€程(man thread)退出,代表進(jìn)程結(jié)束,那么進(jìn)程曾經(jīng)所申請的進(jìn)程地址空間、頁表、代碼和數(shù)據(jù)也會被釋放,雖然新線程是分離的,但是依舊是和主線程共享資源的;所謂分離,僅僅是主線程不需要再等待新線程了,不需要關(guān)心新線程的執(zhí)行結(jié)果,但資源依舊是共享的。
新線程處于分離狀態(tài),新線程無線循環(huán)的跑下去,但是新線程中會出現(xiàn)異常,主線程5秒之后,就退出,會發(fā)生什么事情呢??
- 異常之后,整個進(jìn)程都會退出。
總結(jié)
好了,本篇博客到這里就結(jié)束了,如果有更好的觀點(diǎn),請及時留言,我會認(rèn)真觀看并學(xué)習(xí)。
不積硅步,無以至千里;不積小流,無以成江海。