老河口做網(wǎng)站免費(fèi)的外貿(mào)b2b網(wǎng)站
線程概念
線程這個(gè)詞或多或少大家都聽(tīng)過(guò),今天我們正式的來(lái)談一下線程;
在我一開(kāi)始的概念中線程就是進(jìn)程的一部分,一個(gè)進(jìn)程中有很多個(gè)線程,這個(gè)想法基本是正確的,但細(xì)節(jié)部分呢我們需要細(xì)細(xì)講解一下;
什么是線程
1.線程是進(jìn)程執(zhí)行流中的一部分,就是說(shuō)線程是進(jìn)程內(nèi)部的一個(gè)控制序列;
2.線程是操作系統(tǒng)調(diào)度的基本單位;
3.在linux中沒(méi)有真正意義上的線程,也就是操作系統(tǒng)中說(shuō)的tcb(thread ctrl block),但是其他的操作系統(tǒng)是有的不同的操作系統(tǒng)實(shí)現(xiàn)不同(如windows就是在pcb下再次構(gòu)建了tcb的數(shù)據(jù)結(jié)構(gòu));為什么linux下沒(méi)有真正意義的線程呢?因?yàn)榫€程再操作系統(tǒng)中也是需要被管理的,可是線程的管理一定得創(chuàng)建數(shù)據(jù)結(jié)構(gòu),創(chuàng)建復(fù)雜的數(shù)據(jù)結(jié)構(gòu)一定需要增加維護(hù)的成本與難度,而線程的管理其實(shí)和進(jìn)程是相似的;所以聰明的linux程序員將線程管理設(shè)計(jì)為了輕量化的進(jìn)程,將線程與進(jìn)程統(tǒng)一管理,減輕了代碼的復(fù)雜度,便于維護(hù)提高效率;(線程粒度細(xì)于進(jìn)程)
4.線程其實(shí)是進(jìn)程的一部分,所以線程運(yùn)行的地方就是在進(jìn)程的虛擬地址空間中的;因?yàn)榫€程本身也是屬于進(jìn)程的一部分的,只是被加載到了進(jìn)程隊(duì)列中運(yùn)行而已;(進(jìn)程就像是一個(gè)家庭,線程就像是家庭中的每一個(gè)人,每個(gè)人都有自己的工作,所以需要分開(kāi)執(zhí)行,也就是處于進(jìn)程隊(duì)列中),進(jìn)程會(huì)分配的資源給線程(家庭中的資源會(huì)分配給每個(gè)人,比如爸爸要去遠(yuǎn)的地方工作需要開(kāi)車,那車子這個(gè)資源就會(huì)分配給父親),這個(gè)資源包括代碼和數(shù)據(jù),之前我們理解的進(jìn)程可以當(dāng)作是主線程,通過(guò)分配自己的代碼給它內(nèi)部的線程,內(nèi)部的線程拿到數(shù)據(jù)和代碼資源區(qū)執(zhí)行分配給它的工作,從而執(zhí)行相應(yīng)的操作;
重談虛擬地址空間
頁(yè)表如何映射
計(jì)算頁(yè)表大小
?所以一個(gè)頁(yè)表最大為4mb,并且一個(gè)頁(yè)表的二級(jí)頁(yè)表不一定為1024個(gè),因?yàn)轫?yè)表的映射也不是一次就完成的,而已頁(yè)表的映射使用完之后還會(huì)釋放等;所以一個(gè)頁(yè)表大小不會(huì)大于4mb;
就是這樣的頁(yè)表完成了我們的映射;那我們的數(shù)據(jù)和代碼都是存儲(chǔ)在這個(gè)地址空間上的;而函數(shù)就是一個(gè)現(xiàn)成的地址,所以我們分配給線程代碼數(shù)據(jù),是不是可以直接將這個(gè)函數(shù)分給線程呢?這樣不就等于把線程需要執(zhí)行的工作劃分給了線程嗎?
所以線程劃分資源本質(zhì)上是將地址空間中的資源進(jìn)行分配
為什么我們要?jiǎng)?chuàng)建線程?線程優(yōu)點(diǎn)
1.同一進(jìn)程中線程之間的切換更加輕量化;
在我們的內(nèi)存中最快的是寄存器,,cpu之間拿寄存器中的數(shù)據(jù)進(jìn)行計(jì)算,寄存器也需要獲取數(shù)據(jù),而寄存器不是之間從內(nèi)存中拿數(shù)據(jù)的,因?yàn)閮?nèi)存相較于寄存器還是太慢了,所以它們之間還有一個(gè)cache緩存,這個(gè)cache中存放的是當(dāng)前進(jìn)程的數(shù)據(jù)和指令,寄存器可以很快的就從cache中拿到一個(gè)進(jìn)程中的數(shù)據(jù)(cache命中率會(huì)很高,因?yàn)槎荚谕贿M(jìn)程,都是熱數(shù)據(jù));因?yàn)橥贿M(jìn)程中的線程是共享數(shù)據(jù)的,所以cache切換時(shí)只需要切換task_struct,而進(jìn)程之前切換所有數(shù)據(jù)都需要切換(進(jìn)程切換了,進(jìn)程間具有獨(dú)立性,cache中的數(shù)據(jù)一定都需要被切換,所咦數(shù)據(jù)會(huì)變冷重新去命中數(shù)據(jù)),這樣的切換消耗會(huì)大的多;
2.創(chuàng)建和銷毀線程的代價(jià)要小很多;因?yàn)榫€程的數(shù)據(jù)已經(jīng)在內(nèi)存中了,線程只需要從它所在的進(jìn)程中獲取數(shù)據(jù)即可;
3.io密集型程序,通過(guò)多線程可以提高很大的效率,在進(jìn)行io的時(shí)候進(jìn)程可以讓其他線程進(jìn)行計(jì)算等操作,不需要等待io結(jié)束再操作;相比單線程的等待要優(yōu)化非常多;
4.計(jì)算密集型程序,在單核cpu中多線程沒(méi)有什么提升,想法,線程之間的切換還會(huì)降低效率;但是在多核cpu中,多線程可以在多個(gè)核上進(jìn)行計(jì)算(計(jì)算線程數(shù)要小于等于核的數(shù)量),也是大大提高了計(jì)算的效率的;
線程缺點(diǎn):
由于線程之前沒(méi)有獨(dú)立性,共享進(jìn)程代碼數(shù)據(jù),代碼的健壯性要低一些,所以需要進(jìn)行同步于互斥;缺乏訪問(wèn)控制->健壯性低;相應(yīng)的調(diào)試也會(huì)更難;
線程數(shù)據(jù)?
每個(gè)線程雖然都是進(jìn)程的一部分,從進(jìn)程中獲得數(shù)據(jù)的,但是線程一定需要包含自己的數(shù)據(jù);
線程自己的數(shù)據(jù):
1.線程對(duì)應(yīng)的上下文數(shù)據(jù)(寄存器)
2.線程運(yùn)行時(shí)數(shù)據(jù)(獨(dú)立的棧空間)
3.線程id
4.信號(hào)屏蔽字
5.調(diào)度優(yōu)先級(jí)
?6.errno
線程操作
上面講解了線程的基本內(nèi)容,下面我們來(lái)對(duì)線程進(jìn)行操作來(lái)理解線程;
我們需要先了解這些linux中posix標(biāo)準(zhǔn)中的原生線程庫(kù)中的函數(shù);?
線程創(chuàng)建
pthread_create
這個(gè)函數(shù)是用來(lái)創(chuàng)建子線程的;
第一個(gè)參數(shù)是一個(gè)輸出型參數(shù),用來(lái)輸出創(chuàng)建線程的tid;
第二個(gè)參數(shù)是用來(lái)設(shè)置線程的屬性的,其實(shí)這是一個(gè)指向線程屬性對(duì)象的指針,通過(guò)傳遞我們?cè)O(shè)置好的對(duì)象傳遞給線程從而改變線程的默認(rèn)屬性,一般我們都傳遞NULL使用默認(rèn)屬性即可;
第三個(gè)參數(shù)是一個(gè)回調(diào)函數(shù),用來(lái)提供給線程運(yùn)行的代碼,可以理解為讓線程執(zhí)行此函數(shù);
第四個(gè)參數(shù)就是一個(gè)傳遞給函數(shù)(第三個(gè)參數(shù)——回調(diào)函數(shù))的參數(shù),這個(gè)參數(shù)既可以是普通的內(nèi)置類型,也可以是結(jié)構(gòu)體,這樣可以很多的數(shù)據(jù);
返回值返回0為成功創(chuàng)建,創(chuàng)建失敗返回返回錯(cuò)誤碼,不設(shè)置errno;
?下面可以看到我們的代碼成功運(yùn)行了;
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
using namespace std;void *routine(void *data)
{for (int i = 0; i < 5; i++){usleep(100000);cout << "線程1, pid: " << getpid() << endl;}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, routine, nullptr);for (int i = 0; i < 7; i++){usleep(200000);cout << "線程0, pid: " << getpid() << endl;}return 0;
}
?從上面的現(xiàn)象我們可以清楚的知道線程是一個(gè)獨(dú)立的執(zhí)行流雖然routine函數(shù)和main函數(shù)它們兩個(gè)再同一個(gè)程序中且是兩個(gè)循環(huán)但是,這兩個(gè)循環(huán)同時(shí)跑起來(lái)了,所以證明了線程的獨(dú)立性;
編譯時(shí)需要加-lpthread選項(xiàng)
在linux中使用原生線程庫(kù)進(jìn)行編程時(shí)我們編譯選項(xiàng)總是需要帶上-lpthread,這個(gè)選項(xiàng)在我們前面學(xué)習(xí)動(dòng)靜態(tài)庫(kù)的時(shí)候就很熟悉了,用來(lái)連接指定的庫(kù);而似乎我們?cè)谝酝木幊讨谐_(kāi)我們自己創(chuàng)建動(dòng)靜態(tài)庫(kù)的情況之外,我們從未出現(xiàn)過(guò)主動(dòng)連接動(dòng)靜態(tài)庫(kù)的情況;
為什么我們不需要主動(dòng)去連接呢?這是因?yàn)榫幾g器自動(dòng)去幫我們連接了,我們的c,c++語(yǔ)言級(jí)別的庫(kù)也好,linux的系統(tǒng)庫(kù)也罷,它們庫(kù)的路徑都是已經(jīng)存儲(chǔ)在編譯器的配置文件中的,編譯器可以自動(dòng)的找到庫(kù)(第一步),然后編譯器會(huì)自動(dòng)連接這些庫(kù)(第二步);為什么會(huì)自動(dòng)連接呢?我們可以認(rèn)為這些系統(tǒng)庫(kù)和標(biāo)準(zhǔn)庫(kù)是編譯器自己的庫(kù),所以編譯器會(huì)自動(dòng)的連接;而pthread這個(gè)庫(kù)是posix標(biāo)準(zhǔn)中的原生線程庫(kù);它是屬于第三方庫(kù)的,而第三方庫(kù)即使它被放到系統(tǒng),標(biāo)準(zhǔn)庫(kù)的路徑之下,它也是不會(huì)被自動(dòng)連接的;所以我們需要帶上-lpthread選項(xiàng)去主動(dòng)連接這個(gè)庫(kù);
查看線程
我們看到的線程的現(xiàn)象接下來(lái),我們從系統(tǒng)的角度的入手,使用系統(tǒng)的指令來(lái)查看我們的線程的體現(xiàn);
ps -aL
lwp的全稱是light weight process輕量級(jí)進(jìn)程;?
線程的等待與tid獲取函數(shù)
pthread_join
子進(jìn)程被創(chuàng)建,父進(jìn)程需要等待進(jìn)程返回,而線程被創(chuàng)建也需要被等待,但是這里只有主線程和其他線程的區(qū)別,主線程需要等待其他所有線程,防止內(nèi)存泄漏的問(wèn)題;
?這里的第一個(gè)參數(shù)是指向被等待線程的tid;
第二個(gè)參數(shù)是一個(gè)輸出型參數(shù)可以用來(lái)接收線程的返回值,這個(gè)返回值可以是任意類型的數(shù)據(jù)(自定義類型也可以);
返回值為0代表等待成功,非0則返回錯(cuò)誤值,不設(shè)置errno碼;
pthread_self
可以獲得線程的tid;
這是一個(gè)無(wú)參函數(shù)和getpid的使用方式是一樣的;
代碼實(shí)現(xiàn)?
?知道了這些基本的函數(shù)后,我們下面用代碼實(shí)踐來(lái)展示現(xiàn)象并解釋:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
using namespace std;struct thread_data
{string threadName;string threadReturn;
};void *routine(void *data)
{thread_data *d1 = static_cast<thread_data *>(data);// thread_data *d1 = (thread_data *)(data);int count = 3;for (int i = 0; i < count; i++){printf("tid: %p threadName: %s count: %d\n", pthread_self(), d1->threadName.c_str(), i);sleep(1);}// int a=5/0;//除0錯(cuò)誤 這里說(shuō)明了當(dāng)進(jìn)程中的某個(gè)線程出現(xiàn)異常時(shí),整個(gè)進(jìn)程都會(huì)退出// exit(0);//使用exit退出 這里也說(shuō)明了使用exit會(huì)退出整個(gè)進(jìn)程d1->threadReturn="return_"+d1->threadName;return d1;
}void initThread(thread_data *data, int num)
{data->threadName = "thread_" + to_string(num);
}int main()
{pthread_t tid;thread_data *data = new thread_data;initThread(data, 1);int ret_create = pthread_create(&tid, nullptr, routine, (void *)data);void *ret_thread;printf("我是主線程tid: %p\n",pthread_self());pthread_join(tid, &ret_thread);cout << ((thread_data *)ret_thread)->threadReturn << endl;//證明獲得了一個(gè)類返回值delete data;return 0;
}
?使用return正常退出的情況:
下面是使用exit和異常退出的情況:?
?
通過(guò)代碼和現(xiàn)象我們可以知道這些細(xì)節(jié):
1. 我們可以使用join獲取線程的返回值,線程返回值可以為任意類型的指針,所以可以傳遞任意值;
2.我們的子線程退出的時(shí)候不能使用exit退出這樣會(huì)導(dǎo)致整個(gè)進(jìn)程都退出,我們可以使用return,pthread_exit(后面講),使用cacel取消joined(后面講),這3種方式退出;
3.進(jìn)程中的任意一個(gè)線程出現(xiàn)異常整個(gè)進(jìn)程都會(huì)退出
4.線程的tid是一個(gè)地址,這個(gè)地址是進(jìn)程堆棧之間的內(nèi)存區(qū)域(通過(guò)上面的現(xiàn)象也可清楚的明白)
由此我們可以知道這些函數(shù)的大致使用;
線程結(jié)構(gòu)體位置
上面我們通過(guò)概念與實(shí)現(xiàn)基本的了解了線程,接下來(lái)我們通過(guò)圖像來(lái)了解線程的結(jié)構(gòu)體:
其實(shí)我們的線程是這樣存在在我們的進(jìn)程中的,因?yàn)閘inux程序員為了減輕代碼的維護(hù)效率linux中沒(méi)有真正的線程,而是將線程作為輕量級(jí)進(jìn)程,而用來(lái)描述輕量級(jí)進(jìn)程的結(jié)構(gòu)體是存儲(chǔ)在用戶層的,存儲(chǔ)的位置就是共享區(qū)的原生線程庫(kù),線程庫(kù)中維護(hù)了線程的屬性數(shù)據(jù),內(nèi)核的執(zhí)行流(tcb控制塊)通過(guò)找到進(jìn)程中的線程庫(kù)中的線程結(jié)構(gòu)體從而找到線程代碼執(zhí)行線程;?
所以線程的屬性是由線程庫(kù)來(lái)維護(hù)的,而tid之所以是共享區(qū)之中的代碼的原因就是因?yàn)閠id指的是共享區(qū)中線程庫(kù)中的某個(gè)線程結(jié)構(gòu)體所在的首地址;
線程空間的特點(diǎn)
1.線程之間的??臻g是獨(dú)立的;
這一點(diǎn)非常好理解,因?yàn)楹瘮?shù)在被調(diào)用的時(shí)候就會(huì)創(chuàng)建自己的棧幀嘛;而線程執(zhí)行其實(shí)就是執(zhí)行了分給他的函數(shù);所以線程??臻g是獨(dú)立的;
2.線程之間是沒(méi)有秘密的;
為什么線程之間獨(dú)立但是又沒(méi)有秘密呢?因?yàn)榫€程總是在一個(gè)進(jìn)程中的嘛,棧之間的數(shù)據(jù),只需要通過(guò)一個(gè)指針就可以獲得了;
代碼示例:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <vector>
#include <string>
using namespace std;struct threadData
{string threadName;threadData(int num){threadName = "thread" + to_string(num);}threadData() = default;
};int *g_index;void *routine(void *args)
{int val = 0;threadData *data = (threadData *)args;for (int i = 1; i <=3 ; i++){printf("%s tid: %p val: %d\n", data->threadName.c_str(), pthread_self(), val);val++;}if(data->threadName=="thread1"){val=10000;g_index=&val;sleep(5);}return (void *)0;
}int main()
{vector<pthread_t> tids;for (int i = 0; i < 3; i++){pthread_t tid;threadData *td = new threadData(i);pthread_create(&tid, nullptr, routine, td);tids.push_back(tid);sleep(1);}cout<<"這是thread2的val值: "<<*g_index<<endl;for(auto t:tids){void *retData;pthread_join(t,&retData);}return 0;
}
但是如果我們想要獲得某個(gè)??臻g的數(shù)據(jù)時(shí)這也是可以輕松做到的:
我們?cè)趓outine函數(shù)中加入一段這樣的代碼,并在main函數(shù)中讀取數(shù)據(jù);
routine函數(shù)中:if(data->threadName=="thread1"){val=10000;g_index=&val;sleep(5);}
main函數(shù)中:cout<<"這是thread2的val值: "<<*g_index<<endl;
?
線程的變量:__thread選項(xiàng)?
int *g_index;
//int g_val;
__thread int g_val;void *routine(void *args)
{int val = 0;threadData *data = (threadData *)args;for (int i = 1; i <=3 ; i++){//printf("%s tid: %p val: %d\n", data->threadName.c_str(), pthread_self(), val);//val++;printf("%s g_val: %d\n",data->threadName.c_str(),g_val);g_val++;}// if(data->threadName=="thread1")// {// val=10000;// g_index=&val;// sleep(5);// }return (void *)0;
}int main()
{vector<pthread_t> tids;for (int i = 0; i < 3; i++){pthread_t tid;threadData *td = new threadData(i);pthread_create(&tid, nullptr, routine, td);tids.push_back(tid);sleep(1);}//cout<<"這是thread2的val值: "<<*g_index<<endl;for(auto t:tids){void *retData;pthread_join(t,&retData);}return 0;
}
我們線程在使用 g_val全局變量時(shí):
g_val帶上__thread編譯選項(xiàng)時(shí):
?
__thread是編譯選擇,不是c,c++的語(yǔ)法是編譯器的選項(xiàng);
特點(diǎn):
?1.將進(jìn)程全局?jǐn)?shù)據(jù)變?yōu)榫€程全局?jǐn)?shù)據(jù)
2.只能給內(nèi)置類型帶上這個(gè)選項(xiàng)
C++線程庫(kù)說(shuō)明
在我們的C++中是有語(yǔ)言級(jí)別的線程庫(kù)的(C語(yǔ)言沒(méi)有),C++中的線程庫(kù)是跨平臺(tái)的,但是我們?cè)谑褂肅++線程庫(kù)時(shí),我們還是會(huì)發(fā)現(xiàn),我們需要帶上編譯選項(xiàng)-lpthread所以說(shuō)明C++的線程庫(kù)是封裝了原生線程庫(kù)的,而原生線程庫(kù)在linux中是posix標(biāo)準(zhǔn)的,在windows中又有不同的標(biāo)準(zhǔn);但是C++的線程庫(kù)是跨平臺(tái)的,所以說(shuō)明C++的線程庫(kù)不僅封裝了linux的posix標(biāo)準(zhǔn)線程庫(kù)還封裝了windows下的線程庫(kù);
clone系統(tǒng)調(diào)用的封裝
我們前面說(shuō)線程是輕量級(jí)的進(jìn)程,為什么這么說(shuō)呢?其實(shí)我們?cè)趧?chuàng)建線程時(shí)使用的pthread_create函數(shù)和創(chuàng)建子進(jìn)程的fork函數(shù)都是封裝了clone的系統(tǒng)調(diào)用;
int clone(int (*fn)(void *), void *child_stack
, int flags, void *arg
, ... /* pid_t *ptid, void *tls, pid_t *ctid */);
這個(gè)系統(tǒng)系統(tǒng)調(diào)用會(huì)指定一片棧空間給新開(kāi)辟的線程,我們不需要懂clone調(diào)用的細(xì)節(jié),我們只需要知道,linux中其實(shí)在底層上線程的接口也是和進(jìn)程用的一樣的調(diào)用,所以它們?cè)趦?nèi)核層面上是處于同一級(jí)別的執(zhí)行流的,所以線程被稱為輕量級(jí)進(jìn)程;
小提示:
線程如何使用進(jìn)程替換的調(diào)用會(huì)將當(dāng)前的整個(gè)進(jìn)程替換掉
線程終止
前面我們說(shuō)了線程的3個(gè)正常退出方式;我們下面來(lái)詳細(xì)的講解一下:
pthread_exit
這個(gè)函數(shù)就是和return一樣的作用,返回一個(gè)retval給主線程;這里需要注意的是retval最好是堆上的指針,線程終止棧幀也會(huì)銷毀,會(huì)導(dǎo)致棧上的數(shù)據(jù)被釋放,所以返回值一定要是不被釋放的數(shù)據(jù);
pthread_cancel
這是一個(gè)線程終止函數(shù),我們可以通過(guò)此函數(shù)終止掉tid的線程:
這里終止了就不需要再join了,如果join了會(huì)發(fā)返回非0值;?
這是gpt給出的提示:
盡管?pthread_cancel
?函數(shù)可以請(qǐng)求取消另一個(gè)線程,但是線程是否真正被取消,以及何時(shí)被取消,是由目標(biāo)線程自身來(lái)決定的。目標(biāo)線程可以選擇忽略取消請(qǐng)求,或者在適當(dāng)?shù)臅r(shí)機(jī)響應(yīng)取消請(qǐng)求并執(zhí)行清理操作。
?線程分離
pthread_detach
我們的主線程永遠(yuǎn)是最后退出的,因?yàn)樾枰却袆?chuàng)建進(jìn)程退出,我們常見(jiàn)的服務(wù)器一般都是死循環(huán)不退出的程序;而當(dāng)主線程不關(guān)系創(chuàng)建的線程的結(jié)果時(shí),可以使用detach來(lái)斷開(kāi)創(chuàng)建線程與主線程之間的關(guān)系;,主線程就不需要等待子線程了;
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include<cstring>
using namespace std;void* routine(void*args)
{cout<<"我是被創(chuàng)建線程"<<endl;
}int main()
{pthread_t tid;pthread_create(&tid,nullptr,routine,nullptr);void* ret;pthread_detach(tid);int ret_join=pthread_join(tid,&ret);printf("%s\n",strerror(ret_join));return 0;
}
當(dāng)沒(méi)有detach時(shí):
當(dāng)創(chuàng)建的線程被detach時(shí)
?所以說(shuō)明線程不能被同時(shí)detach和join;
此外線程可以自己detach自己;
以上就是線程的控制與基本概念,線程部分未完待續(xù);?