柚子快報激活碼778899分享:算法 【Linux】線程
柚子快報激活碼778899分享:算法 【Linux】線程
目錄
線程概念
線程控制
在說線程之前,我們需要鋪墊一些背景知識:
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
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
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
while(true)
{
std::cout << name << " is running..." << std::endl;
sleep(1);
break;
}
return args;
}
int main()
{
std::vector
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
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
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
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】線程
文章鏈接
本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點和立場。
轉(zhuǎn)載請注明,如有侵權(quán),聯(lián)系刪除。