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

首頁綜合 正文
目錄

柚子快報激活碼778899分享:算法 【Linux】線程

柚子快報激活碼778899分享:算法 【Linux】線程

http://yzkb.51969.com/

目錄

線程概念

線程控制

在說線程之前,我們需要鋪墊一些背景知識:

1.重談地址空間

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

之前父子進程對數(shù)據(jù)進行修改時會發(fā)生寫時拷貝,而寫時拷貝的基本單位也是4KB,為什么修改很少的數(shù)據(jù)也要拷貝4KB呢?因為如果這個數(shù)據(jù)被修改,那其周圍的數(shù)據(jù)大概率也要被修改,每次都寫時拷貝對OS是一種負(fù)擔(dān),這其實是用空間來換時間。

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

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

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

我們來算一筆賬,頁表里每項2個字節(jié),一個頁表就是2KB,一共有1024張頁表,所以所有頁表加起來一共是2MB,加上一級頁目錄4KB。

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

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

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

線程概念

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

說到這里,可能還不不是很理解,下面就先講一個故事:

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

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

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

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

站在CPU的角度,在Linux中,它所調(diào)度的task_struct<=進程,CPU用不用區(qū)分task_struct是進程還是線程?不用區(qū)分!因為在CPU看起來都是執(zhí)行流,所以CPU看到的執(zhí)行流<=進程,所以Linux中的執(zhí)行流叫做輕量級進程!

話不多說,我們先來用代碼來見一見線程:

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

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

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

//新線程

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");

//主線程

while(1)

{

sleep(1);

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

}

return 0;

}

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

其實,我們可以使用ps -aL查看線程,

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

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

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

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

以上只是說的線程的優(yōu)點,但是它也是存在缺點的。在多線程中,他們共享地址空間、頁表、代碼和數(shù)據(jù),如果其中一個線程出現(xiàn)野指針報錯,就是這個進程出異常了,那這個進程就被干掉了,一個線程崩潰會導(dǎo)致其他線程崩潰。所以,如果代碼如果沒寫好,其健壯性會比較差。而多進程沒有這個問題。進程和線程同時存在時因為它們都有不可取代性。

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

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

下面我們再來說一道常見的面試題,為什么線程的調(diào)度成本更低?

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

?

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

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

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

線程也有很多缺點:

1.健壯性降低。一個線程出問題,那這個進程直接終止,所以其他線程就終止了。

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

Linux中進程和線程對比

1.進程是資源分配的基本單位

2.進程是資源分配的基本單位

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

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

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

線程控制

在我們編譯源文件時,多加了pthread這個庫,為什么會這樣呢?

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

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

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

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

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

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

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

//問題1.main和new線程誰先運行呢?不確定

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.我們期望誰最后退出?main thread ,如何保證?

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

if(n == 0)

{

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

}

return 0;

}

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

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

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

問題4:全面看待線程函數(shù)傳參

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

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

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

//問題1.main和new線程誰先運行呢?不確定

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進制打印出來

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

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

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

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

if(n == 0)

{

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

}

return 0;

}

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

我們再來談這個函數(shù)的第二個參數(shù)retval,這是一個輸出型參數(shù),需要傳一個void*的變量地址,在新線程結(jié)束后,threadrun函數(shù)會返回一個void*的返回值,而這個返回值會被主線程的pthread_join獲取,未來要通過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

//問題1.main和new線程誰先運行呢?不確定

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進制打印出來

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

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

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

void* code = nullptr;

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

if(n == 0)

{

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

}

return 0;

}

我們可以看到,主線程接收到了新線程的退出碼。

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

a.只考慮正確的返回,不考慮異常,因為異常了,整個進程就崩潰了,包括主進程。

b.我們可以傳遞任意類型,也可以傳遞類對象的地址。

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

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

我們直接來看代碼:

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.有線程的id

pthread_t tid;

//2.有線程的名字

char* name = new char[128];

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

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

//3.保存所有線程的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;

}

問題7:新線程如何終止?

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

a.函數(shù)return。不能使用exit終止一個線程,exit是專門用來終止進程的,不能用來終止線程。

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

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

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

#define PTHREAD_CANCELED ((void *) -1)

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

可以!

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

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

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

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

在threadrun函數(shù)中,讓新線程和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);//專門用來終止一個線程的

// exit(1);

}

同樣的,main thread也可以主動和新線程分離,前提是新線程必須存在:

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

#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;

}

如果我們在Makefile只用C++11,

這樣編譯時其實會報錯:

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

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

所以,C++11中的多線程本質(zhì),就是對原生線程庫接口的封裝?。?!

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

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

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

實際上,tid是一個地址,要理解tid,我們首先要理解pthread庫,和動態(tài)庫類似,pthread庫就是存在于磁盤上的一個文件,

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

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

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

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

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

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

那內(nè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),只要新線程改變了全局變量gval的值,主線程也能立即看到。那么,如果gval比較特殊,不能共享,只能讓它們各自單獨擁有一份gval,所以在Linux的g++中,存在__thread,用__pthrea去修飾gval變量,然后再次運行程序,

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

柚子快報激活碼778899分享:算法 【Linux】線程

http://yzkb.51969.com/

文章鏈接

評論可見,查看隱藏內(nèi)容

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

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

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

發(fā)布評論

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

請在主題配置——文章設(shè)置里上傳

掃描二維碼手機訪問

文章目錄