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