欧美free性护士vide0shd,老熟女,一区二区三区,久久久久夜夜夜精品国产,久久久久久综合网天天,欧美成人护士h版

目錄

柚子快報(bào)激活碼778899分享:算法 【Linux】線(xiàn)程

柚子快報(bào)激活碼778899分享:算法 【Linux】線(xiàn)程

http://yzkb.51969.com/

目錄

線(xiàn)程概念

線(xiàn)程控制

在說(shuō)線(xiàn)程之前,我們需要鋪墊一些背景知識(shí):

1.重談地址空間

實(shí)際上,物理內(nèi)存不是一整塊,而是被劃分為一份份4KB空間,OS進(jìn)行內(nèi)存管理,不是以字節(jié)為單位,而是以?xún)?nèi)存塊為單位的,默認(rèn)大小是4KB,4KB是主流Linux操作系統(tǒng)用的大小。實(shí)際上,我們之前學(xué)過(guò),系統(tǒng)和磁盤(pán)文件進(jìn)行IO的基本單位是4KB,也就是8個(gè)扇區(qū),所有計(jì)算機(jī)中的巧合都是被精心設(shè)計(jì)過(guò)的。在程序被編譯完成后,是有地址的,并且以數(shù)據(jù)塊4KB的大小存好了。所謂程序加載,就是把磁盤(pán)上4KB的數(shù)據(jù)塊加載到4KB的內(nèi)存塊上。我們把4KB的內(nèi)存塊叫做頁(yè)框,4KB的數(shù)據(jù)塊叫做頁(yè)幀,?OS對(duì)內(nèi)存的管理工作,基本單位是4KB!

之前父子進(jìn)程對(duì)數(shù)據(jù)進(jìn)行修改時(shí)會(huì)發(fā)生寫(xiě)時(shí)拷貝,而寫(xiě)時(shí)拷貝的基本單位也是4KB,為什么修改很少的數(shù)據(jù)也要拷貝4KB呢?因?yàn)槿绻@個(gè)數(shù)據(jù)被修改,那其周?chē)臄?shù)據(jù)大概率也要被修改,每次都寫(xiě)時(shí)拷貝對(duì)OS是一種負(fù)擔(dān),這其實(shí)是用空間來(lái)?yè)Q時(shí)間。

在4GB內(nèi)存空間中,一共有100多萬(wàn)個(gè)頁(yè)框,OS要將這些頁(yè)框管理起來(lái),先描述、再組織,用struct page管理頁(yè)框,用一個(gè)結(jié)構(gòu)體數(shù)組管理起來(lái),struct page memory[1048576],用數(shù)組管理起來(lái),這樣每一個(gè)page都有了一個(gè)下標(biāo),第一個(gè)page的起始地址是0,下一個(gè)page的起始地址就是1*4KB,以此類(lèi)推,就可以將每一個(gè)page下標(biāo)轉(zhuǎn)換為每一個(gè)內(nèi)存頁(yè)框的起始地址。這樣,對(duì)內(nèi)存的管理就變?yōu)閷?duì)數(shù)組的增刪查改。

另外,我們之前在談頁(yè)表時(shí),并沒(méi)有具體說(shuō),如果是4GB的地址空間,在頁(yè)表中一一將虛擬地址和物理地址對(duì)應(yīng),每組對(duì)應(yīng)關(guān)系再加上一個(gè)狀態(tài)標(biāo)志位(按1個(gè)字節(jié)),那就是一組虛擬地址和物理地址對(duì)應(yīng)關(guān)系要占用9個(gè)字節(jié),那就是36GB,這也太大了吧,所以,實(shí)際上肯定不能這樣映射。那真實(shí)的頁(yè)表是什么樣子的?虛擬地址是如何轉(zhuǎn)換為物理地址的呢?

其實(shí),虛擬地址在OS看來(lái)不是一個(gè)整體,而是將32位拆為10位(2^10=1024)、10位(2^10=1024)、12位(2^12=4096)。首先,將虛擬地址的前10位作為索引(第一張表),所以第一張表一共1024項(xiàng),這張表稱(chēng)為頁(yè)目錄,(頁(yè)目錄最多占4字節(jié)*1024=4KB)頁(yè)目錄中的每一項(xiàng)中存放的內(nèi)容是第二張表(頁(yè)表)的地址,所以頁(yè)目錄可以指向很多張的頁(yè)表,每一張頁(yè)表也是有1024項(xiàng),根據(jù)第二個(gè)10位索引頁(yè)表中的某一項(xiàng),而頁(yè)表中某一項(xiàng)存放的內(nèi)容是指向頁(yè)框的起始地址,然后再拿上虛擬地址的低12位(范圍是[0,4095])作為頁(yè)內(nèi)偏移,是任一個(gè)字節(jié)的偏移量??偨Y(jié)來(lái)看,先拿著虛擬地址的前10位作為頁(yè)目錄的下標(biāo)去索引,通過(guò)頁(yè)目錄這一項(xiàng)的內(nèi)容找到所指向的頁(yè)表,然后通過(guò)虛擬地址的第二個(gè)10位作為下標(biāo)去索引頁(yè)表的某一項(xiàng),這個(gè)頁(yè)表的某一項(xiàng)存放的是指向的頁(yè)框的起始地址,最后根據(jù)虛擬地址的后12位作為某一個(gè)頁(yè)框內(nèi)的偏移量,就可以找到任一個(gè)字節(jié),也就是說(shuō),任意一個(gè)虛擬地址&(0xFFF)==頁(yè)框號(hào)。實(shí)際上,虛擬地址的前20位加起來(lái)的作用是搜索頁(yè)框,在加上虛擬地址的低12位頁(yè)內(nèi)偏移,就能索引到頁(yè)框里的任意一個(gè)字節(jié)。這種分配方案我們稱(chēng)為二級(jí)頁(yè)表。

我們來(lái)算一筆賬,頁(yè)表里每項(xiàng)2個(gè)字節(jié),一個(gè)頁(yè)表就是2KB,一共有1024張頁(yè)表,所以所有頁(yè)表加起來(lái)一共是2MB,加上一級(jí)頁(yè)目錄4KB。

但是現(xiàn)在有些尷尬,我們只能上述方式找到一個(gè)字節(jié)的地址,所以C/C++里對(duì)每個(gè)變量都設(shè)置了類(lèi)型,這樣就能拿到我們想要的任何數(shù)據(jù)。

CPU讀取的是虛擬地址,在CPU中要將虛擬地址根據(jù)頁(yè)表轉(zhuǎn)換為物理地址,那CPU如何找到頁(yè)表呢?實(shí)際上,在CPU中存在cr3,每一個(gè)進(jìn)程會(huì)把自己對(duì)應(yīng)的頁(yè)表中頁(yè)目錄的起始地址放在寄存器當(dāng)中,這樣,虛擬地址被讀到CPU,找到頁(yè)表后,通過(guò)CPU中的MMU電路直接找到虛擬地址,

在地址空間中,正文代碼限定了一批虛擬地址空間的范圍,并且依靠頁(yè)表才能看到對(duì)應(yīng)的實(shí)際物理空間?,F(xiàn)在假設(shè)正文代碼由20個(gè)函數(shù)組成,給 每一個(gè)函數(shù)分配一個(gè)執(zhí)行流,那代碼由并行轉(zhuǎn)為串行,這在技術(shù)上是可行的。我們知道,函數(shù)地址其實(shí)是一批代碼的入口地址,函數(shù)的每行代碼都有地址,而且同一個(gè)函數(shù)我們認(rèn)為地址是連續(xù)的,所以函數(shù)是連續(xù)的代碼地址構(gòu)成的代碼塊,這意味著一個(gè)函數(shù)對(duì)應(yīng)一批連續(xù)的虛擬地址!將這20個(gè)函數(shù)劃分,本質(zhì)上是對(duì)頁(yè)表進(jìn)行劃分,虛擬地址的本質(zhì)是一種資源?。。?/p>

線(xiàn)程概念

先來(lái)說(shuō)一下官方的概念,線(xiàn)程,是在進(jìn)程內(nèi)部運(yùn)行,是CPU調(diào)度的基本單位。之前我們學(xué)過(guò),進(jìn)程之間的代碼數(shù)據(jù)和內(nèi)核數(shù)據(jù)結(jié)構(gòu)之間都是相互獨(dú)立的,那如果創(chuàng)建進(jìn)程時(shí)不再創(chuàng)建地址空間和頁(yè)表,而是只創(chuàng)建一個(gè)新的task_struct,假如正文代碼分成4份,第1份代碼由第1個(gè)進(jìn)程來(lái)執(zhí)行,第二份代碼由第2個(gè)進(jìn)程來(lái)執(zhí)行,然后接著創(chuàng)建第3、4個(gè)進(jìn)程,上面我們描述的就是Linux中的線(xiàn)程,這就是定義的第一句“線(xiàn)程在進(jìn)程內(nèi)部運(yùn)行”。之間我們對(duì)進(jìn)程的定義是進(jìn)程=內(nèi)核數(shù)據(jù)結(jié)構(gòu)+進(jìn)程代碼和數(shù)據(jù),現(xiàn)在我們站在內(nèi)核的角度,給進(jìn)程定義,進(jìn)程是承擔(dān)分配系統(tǒng)資源的基本實(shí)體!

說(shuō)到這里,可能還不不是很理解,下面就先講一個(gè)故事:

在我們偉大的國(guó)家,分配資源的基本實(shí)體是家庭,OS就是國(guó)家,家庭就是OS的一個(gè)個(gè)進(jìn)程,在我們家庭里,會(huì)有爸爸媽媽爺爺奶奶,我們此時(shí)可能在學(xué)校上課,我們執(zhí)行上課的代碼,爸爸媽媽在上班,他們執(zhí)行上班的代碼,爺爺奶奶在遛彎,他們執(zhí)行養(yǎng)老的代碼,我們家庭的每一個(gè)人都在做著自己的事情,我們每一個(gè)人把自己的工作做好,這樣就達(dá)到一個(gè)神奇的效果,就能把這個(gè)家的日子過(guò)好。每一個(gè)家庭的任務(wù)就是把日子過(guò)好,家庭中的每一個(gè)人就是一個(gè)線(xiàn)程。

對(duì)比一下我們之前學(xué)的進(jìn)程,實(shí)際上,我們之前學(xué)的進(jìn)程內(nèi)部只有一個(gè)執(zhí)行流,而現(xiàn)在的進(jìn)程中有多個(gè)執(zhí)行流,所以只有一個(gè)執(zhí)行流的進(jìn)程是有多個(gè)執(zhí)行流的進(jìn)程的特殊情況。

假設(shè)我們現(xiàn)在OS要單獨(dú)設(shè)計(jì)線(xiàn)程,就要設(shè)計(jì)新建、暫停、銷(xiāo)毀、調(diào)度,那線(xiàn)程要不要和進(jìn)程產(chǎn)生關(guān)聯(lián),這里所說(shuō)的幾點(diǎn)就是要管理線(xiàn)程,先描述再組織,struct tcp,tcb結(jié)構(gòu)體里面的成員就要有線(xiàn)程的id,優(yōu)先級(jí),狀態(tài),上下文,鏈接屬性,而描述進(jìn)程的結(jié)構(gòu)體是struct pcb,每個(gè)進(jìn)程有多個(gè)線(xiàn)程,

上圖這種設(shè)計(jì)方案其實(shí)是windows中真實(shí)存在的線(xiàn)程控制塊,CPU在調(diào)度時(shí)先選擇一個(gè)進(jìn)程,再選擇其中一個(gè)線(xiàn)程。然而,在設(shè)計(jì)Linux時(shí),發(fā)現(xiàn)設(shè)計(jì)線(xiàn)程時(shí),線(xiàn)程的各種屬性(id,優(yōu)先級(jí),狀態(tài),上下文,鏈接屬性等)進(jìn)程也都有,那為什么還要單獨(dú)設(shè)計(jì)一個(gè)數(shù)據(jù)結(jié)構(gòu)tcb來(lái)表示線(xiàn)程呢?此外,如果進(jìn)程和線(xiàn)程設(shè)計(jì)成兩套,那調(diào)度算法是不是也要設(shè)計(jì)成兩套?所以,Linux的設(shè)計(jì)者就想能不能復(fù)用pcb,用pcb統(tǒng)一表示執(zhí)行流,這樣的話(huà),我們就不需要為線(xiàn)程單獨(dú)設(shè)計(jì)數(shù)據(jù)結(jié)構(gòu)和調(diào)度算法了,這就是Linux的方案,用進(jìn)程模擬的線(xiàn)程!?。★@然Linux的方案更優(yōu)秀。

站在CPU的角度,在Linux中,它所調(diào)度的task_struct<=進(jìn)程,CPU用不用區(qū)分task_struct是進(jìn)程還是線(xiàn)程?不用區(qū)分!因?yàn)樵贑PU看起來(lái)都是執(zhí)行流,所以CPU看到的執(zhí)行流<=進(jìn)程,所以L(fǎng)inux中的執(zhí)行流叫做輕量級(jí)進(jìn)程!

話(huà)不多說(shuō),我們先來(lái)用代碼來(lái)見(jiàn)一見(jiàn)線(xiàn)程:

在Linux中,使用pthread_create函數(shù)來(lái)創(chuàng)建新的線(xiàn)程,

第一個(gè)參數(shù)thread是一個(gè)線(xiàn)程id,是輸出型參數(shù)。第二個(gè)參數(shù)attr是屬性,一般設(shè)為null,第三個(gè)參數(shù)start_routine是一個(gè)參數(shù)為void*、返回值為void *的函數(shù)指針,一旦線(xiàn)程創(chuàng)建成功,主線(xiàn)程繼續(xù)向下執(zhí)行,新線(xiàn)程執(zhí)行這個(gè)函數(shù)指針?biāo)鶎?duì)應(yīng)的方法。第四個(gè)參數(shù)args是參數(shù)。

?如果線(xiàn)程創(chuàng)建成功返回0,失敗返回錯(cuò)誤碼。

//新線(xiàn)程

void* threadStart(void* args)

{

while(1)

{

std::cout << "new thread running..." << " ,pid: " << getpid() << std::endl;

sleep(1);

}

}

int main()

{

pthread_t tid;

pthread_create(&tid, nullptr, threadStart, (void *)"thread-new");

//主線(xiàn)程

while(1)

{

sleep(1);

std::cout << "main thread running..." << " ,pid: " << getpid() << std::endl;

}

return 0;

}

運(yùn)行以上程序,我們發(fā)現(xiàn)雖然只有一個(gè)進(jìn)程,但是兩個(gè)執(zhí)行流可以一起執(zhí)行,原因就是他們是屬于同一個(gè)進(jìn)程的線(xiàn)程。?

其實(shí),我們可以使用ps -aL查看線(xiàn)程,

我們看到這兩個(gè)線(xiàn)程的pid是一樣的,他倆還有LWP(Light Weight Process,輕量級(jí)進(jìn)程),這就是線(xiàn)程的id。另外,我們發(fā)現(xiàn),其中有一個(gè)的LWP和pid一樣,所以,OS調(diào)度的時(shí)候,看的是pid還是LWP?肯定是LWP。pid和LWP相同的是主線(xiàn)程,不同的是新線(xiàn)程。

現(xiàn)在我們還是有兩個(gè)問(wèn)題:

1.已經(jīng)有多進(jìn)程了,為什么還要有多線(xiàn)程呢?

創(chuàng)建一個(gè)新進(jìn)程,既要?jiǎng)?chuàng)建pcb,開(kāi)辟地址空間,又要?jiǎng)?chuàng)建頁(yè)表,還要加載代碼和數(shù)據(jù),因此創(chuàng)建進(jìn)程成本非常高!而創(chuàng)建線(xiàn)程只需要1.創(chuàng)建一個(gè)pcb 2.把進(jìn)程已有的資源給你,因此,創(chuàng)建線(xiàn)程成本非常低(啟動(dòng))。此外,在運(yùn)行期間,如果切換進(jìn)程,需要保存上下文,切換進(jìn)程頁(yè)表、地址空間,但是在切換線(xiàn)程時(shí),上下文要保護(hù)起來(lái),但是地址空間、頁(yè)表就不用切換了,因此運(yùn)行期間,線(xiàn)程調(diào)度成本低(運(yùn)行)。另外,當(dāng)刪除一個(gè)進(jìn)程時(shí),要釋放pcb、地址空間、代碼和數(shù)據(jù)等,而刪除線(xiàn)程只需要釋放pcb,因此,刪除一個(gè)線(xiàn)程更簡(jiǎn)單(死亡)!

以上只是說(shuō)的線(xiàn)程的優(yōu)點(diǎn),但是它也是存在缺點(diǎn)的。在多線(xiàn)程中,他們共享地址空間、頁(yè)表、代碼和數(shù)據(jù),如果其中一個(gè)線(xiàn)程出現(xiàn)野指針報(bào)錯(cuò),就是這個(gè)進(jìn)程出異常了,那這個(gè)進(jìn)程就被干掉了,一個(gè)線(xiàn)程崩潰會(huì)導(dǎo)致其他線(xiàn)程崩潰。所以,如果代碼如果沒(méi)寫(xiě)好,其健壯性會(huì)比較差。而多進(jìn)程沒(méi)有這個(gè)問(wèn)題。進(jìn)程和線(xiàn)程同時(shí)存在時(shí)因?yàn)樗鼈兌加胁豢扇〈浴?/p>

2.不同系統(tǒng)對(duì)于進(jìn)程和線(xiàn)程的實(shí)現(xiàn)不一樣?為什么OS課本只有1本?

我們來(lái)回顧一下線(xiàn)程的定義,線(xiàn)程,是在進(jìn)程內(nèi)部運(yùn)行,是CPU調(diào)度的基本單位,雖然不同OS對(duì)這句話(huà)的實(shí)現(xiàn)不一樣,但是它們都遵守了線(xiàn)程的定義,所以說(shuō)操作系統(tǒng)是計(jì)算機(jī)界的哲學(xué)。

下面我們?cè)賮?lái)說(shuō)一道常見(jiàn)的面試題,為什么線(xiàn)程的調(diào)度成本更低?

CPU為了加速訪(fǎng)問(wèn)內(nèi)存的效率,CPU中會(huì)存在cache(硬件上),當(dāng)CPU在訪(fǎng)問(wèn)某一行代碼時(shí),會(huì)把這一行附近的相關(guān)代碼和熱點(diǎn)數(shù)據(jù)全部預(yù)先加載到CPU的cache中,這一部分稱(chēng)為進(jìn)程的熱數(shù)據(jù),所以CPU在訪(fǎng)問(wèn)數(shù)據(jù)時(shí),先去cache中去找,找到了就直接從cache中拿數(shù)據(jù),沒(méi)找到才會(huì)去內(nèi)存中拿數(shù)據(jù),然后將這個(gè)數(shù)據(jù)置換到cache中,我們通過(guò)lscpu指令可以查看CPU中的緩存,

?

這就意味著,如果切換進(jìn)程,除了切換pcb、地址空間、頁(yè)表,對(duì)于cache中的熱點(diǎn)數(shù)據(jù),切換后的進(jìn)程用不上,此前保存的數(shù)據(jù)全部作廢,切換進(jìn)程后,catch里的數(shù)據(jù)要重新寫(xiě)入,這個(gè)過(guò)程就比較慢了。而切換線(xiàn)程時(shí),cache之前保存的熱點(diǎn)數(shù)據(jù)可能會(huì)用到,不要丟棄所以也就不需要重新加載熱數(shù)據(jù),所以線(xiàn)程切換效率高。

所以,一個(gè)線(xiàn)程去執(zhí)行一個(gè)函數(shù)的本質(zhì),就是這個(gè)線(xiàn)程擁有了正文代碼的一小塊,就是拿到了一小部分虛擬地址空間范圍,也就是只使用頁(yè)表的一部分,每個(gè)線(xiàn)程各自使用一小部分虛擬地址,所以虛擬地址本質(zhì)上就是一種資源。比如一個(gè)線(xiàn)程拿了20行代碼,另一個(gè)拿了30行代碼,不就是也把頁(yè)表拿了一部分嗎。

如果是計(jì)算密集型應(yīng)用,并不是創(chuàng)建的線(xiàn)程越多越好,而是謹(jǐn)慎創(chuàng)建合適的數(shù)量,一般是和CPU的核數(shù)一樣。如果是IO密集型應(yīng)用,可以允許多創(chuàng)建一些線(xiàn)程。

線(xiàn)程也有很多缺點(diǎn):

1.健壯性降低。一個(gè)線(xiàn)程出問(wèn)題,那這個(gè)進(jìn)程直接終止,所以其他線(xiàn)程就終止了。

2.缺乏訪(fǎng)問(wèn)控制。由于大部分地址空間上的內(nèi)容都是共享的,每個(gè)線(xiàn)程都能看到,一個(gè)線(xiàn)程可能把另一個(gè)線(xiàn)程的數(shù)據(jù)修改。

Linux中進(jìn)程和線(xiàn)程對(duì)比

1.進(jìn)程是資源分配的基本單位

2.進(jìn)程是資源分配的基本單位

3.線(xiàn)程共享進(jìn)程數(shù)據(jù),但也擁有自己的一部分?jǐn)?shù)據(jù),如線(xiàn)程ID、一組寄存器、棧、errno、信號(hào)屏蔽字、調(diào)度優(yōu)先級(jí)等。

其中,線(xiàn)程私有的部分中,一組寄存器和棧是最重要的,一組寄存器中存放的是硬件上下文數(shù)據(jù),這反應(yīng)了線(xiàn)程可以動(dòng)態(tài)運(yùn)行;棧,線(xiàn)程在運(yùn)行的時(shí)候,會(huì)形成各種臨時(shí)變量,臨時(shí)變量會(huì)被每個(gè)線(xiàn)程保存在自己的棧區(qū)。

進(jìn)程的多個(gè)線(xiàn)程共享同一地址空間,因此代碼段、數(shù)據(jù)段都是共享的,如果定義一個(gè)函數(shù),在各線(xiàn)程中都可以調(diào)用,如果定義一個(gè)全局變量,在各線(xiàn)程中都可以訪(fǎng)問(wèn)到,除此之外,各線(xiàn)程還共享以下進(jìn)程資源和環(huán)境:

線(xiàn)程控制

在我們編譯源文件時(shí),多加了pthread這個(gè)庫(kù),為什么會(huì)這樣呢?

其實(shí)在Linux中,不存在線(xiàn)程,而只有輕量級(jí)進(jìn)程,而作為OS學(xué)習(xí)者,只學(xué)過(guò)創(chuàng)建進(jìn)程、終止進(jìn)程、調(diào)度進(jìn)程、等待進(jìn)程,但是Linux系統(tǒng)只會(huì)給上層用戶(hù)提供創(chuàng)建輕量級(jí)進(jìn)程的接口,所以就需要存在中間軟件層--pthread庫(kù),是Linux系統(tǒng)自帶的,原生線(xiàn)程庫(kù),對(duì)輕量級(jí)進(jìn)程接口進(jìn)行封裝,按照線(xiàn)程的接口方式,交給用戶(hù),這樣就保持了Linux系統(tǒng)的純粹性。

線(xiàn)程創(chuàng)建、線(xiàn)程等待、線(xiàn)程終止

在線(xiàn)程創(chuàng)建時(shí),需要包含pthread.h,并且在編譯時(shí)要加上-pthread庫(kù)。在新線(xiàn)程創(chuàng)建完成后,主線(xiàn)程會(huì)繼續(xù)向后執(zhí)行,新線(xiàn)程轉(zhuǎn)而會(huì)去執(zhí)行void* threadrun(void* args)對(duì)應(yīng)的代碼,所以執(zhí)行流就一分為二,實(shí)際上是并行運(yùn)行,pthread_create的第四個(gè)參數(shù)args傳給threadrun做參數(shù)傳入。

同樣的,在創(chuàng)建好線(xiàn)程之后,還需要對(duì)線(xiàn)程進(jìn)行等待,使用pthread_join,

一般是由主線(xiàn)程等待新線(xiàn)程,其第一個(gè)參數(shù)thread就是pthread_create的第一個(gè)參數(shù)的返回值,第二個(gè)參數(shù)一般設(shè)為nullptr,這個(gè)參數(shù)實(shí)際上是為了得到threadrun的返回值。這個(gè)函數(shù)返回值的含義和pthread_create一樣。

寫(xiě)了上面這段代碼,我們問(wèn)題1來(lái)了,main和new線(xiàn)程誰(shuí)先運(yùn)行呢?實(shí)際上是不確定的!問(wèn)題2:我們期望誰(shuí)最后退出?我們期望主進(jìn)程最后退出,因?yàn)楦高M(jìn)程要回收子進(jìn)程的退出信息!那如何來(lái)保證main最后退出呢?就是通過(guò)pthread_join保證,new線(xiàn)程不退,main就阻塞式等待new線(xiàn)程退出。如果main不join,那主線(xiàn)程運(yùn)行完退出,進(jìn)程就退出,所有其他線(xiàn)程也就退出了,所以強(qiáng)烈不推薦這種做法,因?yàn)橹骶€(xiàn)程退出了,new線(xiàn)程還沒(méi)把任務(wù)執(zhí)行完。那如果主線(xiàn)程不退出,也不join,此時(shí)就會(huì)造成類(lèi)似于僵尸進(jìn)程的問(wèn)題,new線(xiàn)程退出時(shí),其所對(duì)應(yīng)的資源也就會(huì)被維護(hù)起來(lái),維護(hù)起來(lái)就是等mian線(xiàn)程去拿new線(xiàn)程的返回值,如果mian一直不拿,就會(huì)造成類(lèi)似僵尸進(jìn)程。

void* threadRun(void* args)

{

int cnt = 10;

while(cnt)

{

std::cout << "new thread run ...,cnt : " << cnt-- << std::endl;

sleep(1);

}

}

int main()

{

pthread_t tid;//unsigned long int

//問(wèn)題1.main和new線(xiàn)程誰(shuí)先運(yùn)行呢?不確定

int n = pthread_create(&tid,nullptr,threadRun,(void*)"thread-1");

if(n != 0)

{

std::cerr << "create thread error" << std::endl;

return 1;

}

std::cout << "main thread join begin..." << std::endl;

//2.我們期望誰(shuí)最后退出?main thread ,如何保證?

n = pthread_join(tid,nullptr);//join來(lái)保證

if(n == 0)

{

std::cout << "main thread wait success" << std::endl;

}

return 0;

}

問(wèn)題3:創(chuàng)建了進(jìn)程就有了tid,tid是什么樣子?是什么呢?

我們看到,線(xiàn)程的id是紅框里的一大串,那我們之間看到LWP好像并不是這樣,

那線(xiàn)程id是什么呢?其實(shí)是一個(gè)虛擬地址,關(guān)于這點(diǎn)我們下面再談。

問(wèn)題4:全面看待線(xiàn)程函數(shù)傳參

在這里我們傳的是(void*)"thread-1",這個(gè)(void*)"thread-1"傳給了threadRun的args,運(yùn)行程序,發(fā)現(xiàn)新線(xiàn)程接收到了這個(gè)參數(shù):

然而,這里要說(shuō)的是,這個(gè)參數(shù)并不是只能傳字符串、整數(shù)等,我們也可以傳遞類(lèi)對(duì)象的地址,

class ThreadData

{

public:

std::string _name;

int _num;

//other

};

void* threadRun(void* args)

{

ThreadData* td = static_cast(args);

int cnt = 10;

while(cnt)

{

std::cout<< td->_name << " run ...,num is " << td->_num << " cnt : " << cnt-- << std::endl;

sleep(1);

}

return nullptr;

}

std::string PrintToHex(pthread_t& tid)

{

char buffer[64];

snprintf(buffer,sizeof(buffer),"0x%lx",tid);

return buffer;

}

int main()

{

pthread_t tid;//unsigned long int

//問(wèn)題1.main和new線(xiàn)程誰(shuí)先運(yùn)行呢?不確定

ThreadData td;

td._name = "thread-1";

td._num = 1;

int n = pthread_create(&tid,nullptr,threadRun,(void*)&td);

if(n != 0)

{

std::cerr << "create thread error" << std::endl;

return 1;

}

std::string tid_str = PrintToHex(tid);//按照16進(jìn)制打印出來(lái)

std::cout << "tid : " << tid_str << std::endl;

std::cout << "main thread join begin..." << std::endl;

//2.我們期望誰(shuí)最后退出?main thread ,如何保證?

n = pthread_join(tid,nullptr);//join來(lái)保證

if(n == 0)

{

std::cout << "main thread wait success" << std::endl;

}

return 0;

}

這樣,我們就可以給線(xiàn)程傳遞多個(gè)參數(shù),甚至方法了。但是main函數(shù)中的ThreadData td;屬于在棧上開(kāi)辟的空間,所以新線(xiàn)程訪(fǎng)問(wèn)的是主線(xiàn)程棧上的變量,這種做法不太推薦,一方面,破壞了主線(xiàn)程的完整性和獨(dú)立性,另一方面,如果再來(lái)一個(gè)新線(xiàn)程,這個(gè)新線(xiàn)程通過(guò)傳參還是可以訪(fǎng)問(wèn)到這個(gè)變量,一個(gè)線(xiàn)程把這個(gè)變量改了不就影響另一個(gè)線(xiàn)程了嗎?所以,我們推薦在堆上申請(qǐng)空間,然后把在堆上申請(qǐng)的空間地址拷貝給線(xiàn)程,堆空間一旦被申請(qǐng)出來(lái),其實(shí)其他線(xiàn)程也能看到,但必須得有地址,把這個(gè)地址交給一個(gè)線(xiàn)程,未來(lái)如果有第二個(gè)線(xiàn)程,就子啊堆上重新new一塊空間交給線(xiàn)程,這樣每個(gè)線(xiàn)程都有一塊堆空間,這樣多線(xiàn)程就不會(huì)互相干擾了。

我們?cè)賮?lái)談這個(gè)函數(shù)的第二個(gè)參數(shù)retval,這是一個(gè)輸出型參數(shù),需要傳一個(gè)void*的變量地址,在新線(xiàn)程結(jié)束后,threadrun函數(shù)會(huì)返回一個(gè)void*的返回值,而這個(gè)返回值會(huì)被主線(xiàn)程的pthread_join獲取,未來(lái)要通過(guò)void* ret,把&ret傳給pthread_join,從而接收到threadrun的返回值。

class ThreadData

{

public:

std::string _name;

int _num;

//other

};

void* threadRun(void* args)

{

ThreadData* td = static_cast(args);

int cnt = 10;

while(cnt)

{

std::cout<< td->_name << " run ...,num is " << td->_num << " cnt : " << cnt-- << std::endl;

sleep(1);

}

delete td;

return (void*)0;

}

std::string PrintToHex(pthread_t& tid)

{

char buffer[64];

snprintf(buffer,sizeof(buffer),"0x%lx",tid);

return buffer;

}

int main()

{

pthread_t tid;//unsigned long int

//問(wèn)題1.main和new線(xiàn)程誰(shuí)先運(yùn)行呢?不確定

ThreadData* td = new ThreadData();

td->_name = "thread-1";

td->_num = 1;

int n = pthread_create(&tid,nullptr,threadRun,(void*)td);

if(n != 0)

{

std::cerr << "create thread error" << std::endl;

return 1;

}

std::string tid_str = PrintToHex(tid);//按照16進(jìn)制打印出來(lái)

std::cout << "tid : " << tid_str << std::endl;

std::cout << "main thread join begin..." << std::endl;

//2.我們期望誰(shuí)最后退出?main thread ,如何保證?

void* code = nullptr;

n = pthread_join(tid,&code);//join來(lái)保證

if(n == 0)

{

std::cout << "main thread wait success,new thread exit code : " << (uint64_t)code << std::endl;

}

return 0;

}

我們可以看到,主線(xiàn)程接收到了新線(xiàn)程的退出碼。

問(wèn)題5:如何全面看待線(xiàn)程函數(shù)返回

a.只考慮正確的返回,不考慮異常,因?yàn)楫惓A?,整個(gè)進(jìn)程就崩潰了,包括主進(jìn)程。

b.我們可以傳遞任意類(lèi)型,也可以傳遞類(lèi)對(duì)象的地址。

所以,這里我們應(yīng)該能理解,為什么pthread_create的第三個(gè)參數(shù)函數(shù)指針的參數(shù)是void*、返回值是void*,這樣我們就能傳入任意類(lèi)型、返回任意類(lèi)型。

問(wèn)題6:如何創(chuàng)建多線(xiàn)程呢?

我們直接來(lái)看代碼:

std::string PrintToHex(pthread_t& tid)

{

char buffer[64];

snprintf(buffer,sizeof(buffer),"0x%lx",tid);

return buffer;

}

const int num = 10;

void* threadrun(void* args)

{

std::string name = static_cast(args);

while(true)

{

std::cout << name << " is running..." << std::endl;

sleep(1);

break;

}

return args;

}

int main()

{

std::vector tids;

for(int i = 0 ; i < num ; i++)

{

//1.有線(xiàn)程的id

pthread_t tid;

//2.有線(xiàn)程的名字

char* name = new char[128];

snprintf(name,128,"thread-%d",i+1);

pthread_create(&tid,nullptr,threadrun,/*線(xiàn)程名字*/name);

//3.保存所有線(xiàn)程的id信息

tids.emplace_back(tid);

}

//join to to

for(auto tid : tids)

{

// std::cout << PrintToHex(tid) << " quit..." << std::endl;

void* name = nullptr;

pthread_join(tid,&name);

std::cout << (const char*)name << " quit..." << std::endl;

delete (const char*)name;

}

return 0;

}

問(wèn)題7:新線(xiàn)程如何終止?

我們知道,main函數(shù)結(jié)束,main thread結(jié)束,表示進(jìn)程結(jié)束,而新線(xiàn)程結(jié)束,只代表自己結(jié)束了。

a.函數(shù)return。不能使用exit終止一個(gè)線(xiàn)程,exit是專(zhuān)門(mén)用來(lái)終止進(jìn)程的,不能用來(lái)終止線(xiàn)程。

b.接口pthread_exit。這個(gè)接口等于線(xiàn)程內(nèi)部的return。

c.main thread調(diào)用接口pthread_cancel。線(xiàn)程能被取消,前提是線(xiàn)程得存在。

一般都是主線(xiàn)程取消新線(xiàn)程。新線(xiàn)程被取消的退出結(jié)果是-1。-1的定義如下:

#define PTHREAD_CANCELED ((void *) -1)

問(wèn)題8:可不可以不join新線(xiàn)程,讓他執(zhí)行完就退出呢?

可以!

pthread_datach函數(shù),即線(xiàn)程分離,

a.一個(gè)線(xiàn)程被創(chuàng)建,默認(rèn)是joinable的,必須要被join的。

b.如果一個(gè)線(xiàn)程被分離,線(xiàn)程的工作狀態(tài)是分離狀態(tài),不需要/不能被join的,依舊屬于進(jìn)程內(nèi)部,但是不需要被等待了。

現(xiàn)在,新線(xiàn)程要主動(dòng)和主線(xiàn)程分離,先來(lái)在認(rèn)識(shí)一個(gè)函數(shù)pthread_self,類(lèi)似與getpid,哪個(gè)線(xiàn)程調(diào)用pthread_self,就返回哪個(gè)線(xiàn)程自己的id。

在threadrun函數(shù)中,讓新線(xiàn)程和main thread分離:?

void* threadrun(void* args)

{

pthread_detach(pthread_self());//和main thread分離

std::string name = static_cast(args);

while(true)

{

std::cout << name << " is running..." << std::endl;

sleep(3);

break;

}

// return args;

pthread_exit(args);//專(zhuān)門(mén)用來(lái)終止一個(gè)線(xiàn)程的

// exit(1);

}

同樣的,main thread也可以主動(dòng)和新線(xiàn)程分離,前提是新線(xiàn)程必須存在:

說(shuō)完Linux下的多線(xiàn)程,我們現(xiàn)在有一個(gè)小插曲,其實(shí)C++11也有多線(xiàn)程,也有其原生線(xiàn)程庫(kù),我們使用C++11創(chuàng)建線(xiàn)程,

#include

#include

#include

#include

#include

#include

#include

void threadrun(int num)

{

while(num)

{

std::cout << "thread-1: " << " num : " << num << std::endl;

num--;

sleep(1);

}

}

int main()

{

std::string name = "thread-1";

std::thread mythread(threadrun,10);

while(true)

{

std::cout << "main thread..." << std::endl;

sleep(1);

}

mythread.join();

return 0;

}

如果我們?cè)贛akefile只用C++11,

這樣編譯時(shí)其實(shí)會(huì)報(bào)錯(cuò):

所以,C++11中創(chuàng)建多線(xiàn)程編譯時(shí),也要加-lpthread,

然后,編譯運(yùn)行以上程序,我們看到結(jié)果:

所以,C++11中的多線(xiàn)程本質(zhì),就是對(duì)原生線(xiàn)程庫(kù)接口的封裝?。。?/p>

實(shí)際上,無(wú)論Linux、Windows還是MacOS,每一款操作系統(tǒng)都有自己對(duì)應(yīng)的創(chuàng)建進(jìn)程方案,但為什么說(shuō)語(yǔ)言具有跨平臺(tái)性呢?因?yàn)闊o(wú)論在什么平臺(tái)下,C++11代碼都是一樣的,在Linux、Windows、MacOS中都提供了C++11的庫(kù),對(duì)上都提供了一樣的線(xiàn)程接口,所以語(yǔ)言上是同一種創(chuàng)建線(xiàn)程的方式,但是每一種平臺(tái)實(shí)現(xiàn)庫(kù)的方式肯定是不一樣的。所以未來(lái)任何語(yǔ)言,只需要把原生線(xiàn)程庫(kù)的接口學(xué)懂了,上層語(yǔ)言只需要熟悉接口就可以了。文件操作,也是如此!

到現(xiàn)在,我們接著談問(wèn)題3中提到的tid到底是什么?我們寫(xiě)了如下代碼,編譯運(yùn)行,

std::string ToHex(pthread_t tid)

{

char buffer[128];

snprintf(buffer,sizeof(buffer),"0x%lx",tid);

return buffer;

}

void* threadrun(void* args)

{

std::string name = static_cast(args);

while(true)

{

std::cout << name << " is running...,tid: " << ToHex(pthread_self()) << std::endl;

sleep(1);

}

}

int main()

{

pthread_t tid;

pthread_create(&tid,nullptr,threadrun,(void*)"thread-1");

std::cout << "new thread running,tid: " << ToHex(tid) << std::endl;

pthread_join(tid,nullptr);

return 0;

}

我們發(fā)現(xiàn)用戶(hù)級(jí)所看到的tid值和其LWP肯定不相等,所以這里我們得出一個(gè)結(jié)論:給用戶(hù)提供的線(xiàn)程ID,不是內(nèi)核中的lwp,而是由pthread庫(kù)自己維護(hù)的一個(gè)唯一值,其實(shí)也好理解,LWP是輕量級(jí)進(jìn)程的ID,不需要呈現(xiàn)給用戶(hù),庫(kù)內(nèi)部也要承擔(dān)對(duì)線(xiàn)程的管理。

實(shí)際上,tid是一個(gè)地址,要理解tid,我們首先要理解pthread庫(kù),和動(dòng)態(tài)庫(kù)類(lèi)似,pthread庫(kù)就是存在于磁盤(pán)上的一個(gè)文件,

mythread也是磁盤(pán)上的一個(gè)文件(可執(zhí)行程序),mythread運(yùn)行時(shí),要首先被加載到內(nèi)存上,在內(nèi)存上就要有自己的代碼和數(shù)據(jù),然后也要配套有pcb、地址空間和頁(yè)表,CPU調(diào)度這個(gè)程序時(shí)就會(huì)去執(zhí)行內(nèi)部代碼。接下來(lái)創(chuàng)建線(xiàn)程,要提前把庫(kù)加載到內(nèi)存,映射到我進(jìn)程的地址空間?。。【唧w來(lái)說(shuō),就是映射到地址空間的堆棧之間的共享區(qū),未來(lái)想訪(fǎng)問(wèn)pthread庫(kù)中任意一個(gè)函數(shù)的地址,都能通過(guò)頁(yè)表找到對(duì)應(yīng)的方法。

而我們可能正在運(yùn)行多個(gè)和mythread一樣的程序,此時(shí)只需要把它地址空間的共享區(qū)通過(guò)頁(yè)表映射到已經(jīng)加載到內(nèi)存中的pthread庫(kù),此時(shí)多個(gè)進(jìn)程就能使用同一個(gè)庫(kù)里的方法來(lái)進(jìn)行線(xiàn)程創(chuàng)建了,所以pthread庫(kù)叫做共享庫(kù),這樣每一個(gè)進(jìn)程都可以只用pthread共享庫(kù)來(lái)創(chuàng)建多線(xiàn)程了。具體示意圖如下:

實(shí)際上,Linux維護(hù)的是輕量級(jí)進(jìn)程的屬性,可是在用戶(hù)層用的是線(xiàn)程,我要的是線(xiàn)程的ID、狀態(tài)等屬性,可是與線(xiàn)程相關(guān)的屬性在Linux內(nèi)核中是沒(méi)有的,所以線(xiàn)程相關(guān)的屬性就要由庫(kù)進(jìn)行維護(hù),pthread庫(kù)對(duì)線(xiàn)程也具有分配ID的功能,要承擔(dān)對(duì)線(xiàn)程的管理,所以pthread庫(kù)如何做到對(duì)線(xiàn)程管理呢?先描述,再組織!我們?cè)谑褂胮thread_create的時(shí)候,什么叫做創(chuàng)建線(xiàn)程呢?創(chuàng)建線(xiàn)程又做了什么呢?只有內(nèi)核中的LWP是不夠的,LWP中不包括任何包含線(xiàn)程的概念。所以創(chuàng)建線(xiàn)程時(shí),pthread庫(kù)會(huì)為我們創(chuàng)建一個(gè)上圖中的結(jié)構(gòu)(其實(shí)就是一個(gè)結(jié)構(gòu)體),也就是申請(qǐng)了一個(gè)內(nèi)存塊,其中第一項(xiàng)struct pthread中存放了線(xiàn)程在用戶(hù)級(jí)最基本的屬性,第三項(xiàng)線(xiàn)程棧就是用戶(hù)級(jí)線(xiàn)程所擁有的獨(dú)立的棧結(jié)構(gòu)。每創(chuàng)建一個(gè)線(xiàn)程就給我們申請(qǐng)這樣的內(nèi)存塊,所有的內(nèi)存塊連續(xù)存放。所謂先描述,這個(gè)結(jié)構(gòu)體包含了庫(kù)中創(chuàng)建描述線(xiàn)程的相關(guān)結(jié)構(gòu)體字段屬性;所謂再組織,可以先理解為把內(nèi)存塊用數(shù)組的形式管理起來(lái)。換言之,未來(lái)如果我們想找一個(gè)線(xiàn)程的所有屬性,我們只需要找到線(xiàn)程控制塊的地址就可以了,所以pthread_t id就是一個(gè)地址,就是線(xiàn)程控制塊的地址!怎么理解呢?我們之間學(xué)fopen的時(shí)候,其返回值FILE*中的FILE是一個(gè)結(jié)構(gòu)體,里面包含了文件的相關(guān)屬性,那這個(gè)FILE對(duì)象在哪里呢?在C標(biāo)準(zhǔn)庫(kù)里!所以我們?cè)L問(wèn)文件對(duì)象不就是在拿著地址訪(fǎng)問(wèn)文件對(duì)象嗎?

所以,我們應(yīng)該能理解,在pthread_join的時(shí)候,是在拿著線(xiàn)程ID找到對(duì)應(yīng)的線(xiàn)程控制塊,然后從里面取出線(xiàn)程返回值給*retval,再釋放對(duì)應(yīng)的線(xiàn)程控制塊,所以這就是為什么我們能拿到線(xiàn)程退出結(jié)果的原因。

因此,Linux線(xiàn)程=pthread庫(kù)中線(xiàn)程的屬性集+LWP,是1:1的。那lwp線(xiàn)程在運(yùn)行怎么怎么保證把自己產(chǎn)生的臨時(shí)變量存放在自己的線(xiàn)程棧上呢?既然有l(wèi)wp概念,那么必然后lwp的系統(tǒng)調(diào)用,比如clone:

lwp可以調(diào)用clone第二個(gè)參數(shù)就可以指定??臻g,而pthread庫(kù)內(nèi)部就是類(lèi)似對(duì)這種系統(tǒng)調(diào)用的封裝。

那內(nèi)存控制塊中的線(xiàn)程局部存儲(chǔ)是干什么用的呢?我們來(lái)看以下程序:

int gval = 100;

std::string ToHex(pthread_t tid)

{

char buffer[128];

snprintf(buffer, sizeof(buffer), "0x%lx", tid);

return buffer;

}

void *threadrun(void *args)

{

std::string name = static_cast(args);

while (true)

{

std::cout << name << " is running...,tid: " << ToHex(pthread_self()) << " ,gval:" << gval << " ,&gval:" << &gval << std::endl;

gval++;

sleep(1);

}

}

int main()

{

pthread_t tid;

pthread_create(&tid, nullptr, threadrun, (void *)"thread-1");

while (true)

{

std::cout << "main thread running ,gval:" << gval << " ,&gval:" << &gval << std::endl;

sleep(1);

}

pthread_join(tid, nullptr);

return 0;

}

我們發(fā)現(xiàn),只要新線(xiàn)程改變了全局變量gval的值,主線(xiàn)程也能立即看到。那么,如果gval比較特殊,不能共享,只能讓它們各自單獨(dú)擁有一份gval,所以在Linux的g++中,存在__thread,用__pthrea去修飾gval變量,然后再次運(yùn)行程序,

原因就在于,在編譯時(shí),一旦一個(gè)內(nèi)置變量被__thread修飾,這樣就在每個(gè)線(xiàn)程的線(xiàn)程控制塊中各自存一個(gè)gval,這就叫線(xiàn)程局部存儲(chǔ)。注意,__thread只在Linux下有效,并且只能修飾內(nèi)置類(lèi)型。

柚子快報(bào)激活碼778899分享:算法 【Linux】線(xiàn)程

http://yzkb.51969.com/

文章鏈接

評(píng)論可見(jiàn),查看隱藏內(nèi)容

本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點(diǎn)和立場(chǎng)。

轉(zhuǎn)載請(qǐng)注明,如有侵權(quán),聯(lián)系刪除。

本文鏈接:http://m.gantiao.com.cn/post/19616773.html

發(fā)布評(píng)論

您暫未設(shè)置收款碼

請(qǐng)?jiān)谥黝}配置——文章設(shè)置里上傳

掃描二維碼手機(jī)訪(fǎng)問(wèn)

文章目錄