如何用源代碼做網(wǎng)站手機優(yōu)化什么意思
上一期我們進行了線程控制的了解與相關(guān)操作,但是仍舊有一些問題沒有解決
本章第一階段就是解決tid的問題,第二階段是進行模擬一個簡易線程庫(為了加深對于C++庫封裝linux原生線程的理解),第三階段就是互斥。
目錄
- 線程id:
- LWP與tid:
- 動態(tài)庫的加載:
- 線程id:
- 如何理解維護在庫中:
- 再次感受一下pthread_join():
- 線程局部存儲:
- 封裝線程庫:
- 封裝:
- 互斥:
線程id:
LWP與tid:
我們還是先來寫一段簡單的代碼進行驗證一下LWP與線程id的關(guān)系。
代碼:
驗證結(jié)果:
足以觀察到LWP與tid的差距是非常大的。
這說明給用戶提供的線程id并不是內(nèi)核中的LWP,而是自己維護的一個唯一值。
自己就是pthread庫。
雖然剛開始覺得不符合常理,但仔細想一想而本該如此:
因為linux并沒有線程,但是我們用戶需要線程的概念,所以pthread庫充當了一個中間角色,封裝linux中的輕量級進程,因此,并不需要呈現(xiàn)給用戶LWP的值,給用戶呈現(xiàn)自己封裝的線程id即可。就像C語言中的FILE,我們直接用庫封裝好的,并不需要在使用文件描述符fd了,也不需要展現(xiàn)給用戶。
因為庫提供了線程id,所以庫也要對pthread進行管理,怎么理解呢?
可以理解為學校給你提供了學號,所以學校要對你進行管理。
我們的linux肯定提供了輕量級進程的調(diào)度系統(tǒng)調(diào)用,但是一個線程不僅僅需要被調(diào)度,也需要一個id,棧大小,被誰啟動…這些屬性也是由庫做管理
的!
針對管理我們要展開一下。
動態(tài)庫的加載:
那就要先看一下線程庫的加載,首先動態(tài)庫和我們的程序肯定都是在磁盤上的文件。
當我們./
運行時,會建立內(nèi)核數(shù)據(jù)結(jié)構(gòu) + 加載數(shù)據(jù)與代碼。
當我們執(zhí)行到pthread_create時,因為我們還未加載動態(tài)庫,會觸發(fā)缺頁中斷,去加載動態(tài)庫,再將動態(tài)庫映射到共享區(qū)。
此時我們就可以正常去執(zhí)行我們的pthread_create去創(chuàng)建線程了。
而我們也說過庫需要對我們的線程id,棧的大小…進行維護,也就是進行管理
而管理就需要對該對象進行描述再進行組織,下圖就是描述他的結(jié)構(gòu)體pthread_t。
其中的struct pthread是用戶最基本的線程屬性,線程局部存儲我們稍后再來進行講解。而編程棧就是我們常說的每個線程都有一個獨立的線程棧!
組織我們可以看成是使用一個數(shù)組進行組織起來的。
線程id:
所以以后想找線程屬性,擁有地址即可進行管理,而我們的tid就是相應(yīng)的pthread_t的地址。
如何理解維護在庫中:
我們還是以FILE進行舉例。
我們的FILE是一個結(jié)構(gòu)體:
struct FILE
{int fd;char buff[N];...
}
我們打開一個文件會得到一個FILE的指針。
而這個FILE結(jié)構(gòu)體指針就維護在標準庫中,進入這個函數(shù)時,會執(zhí)行malloc(sizeof(struct FILE))
類似的代碼在堆上申請空間,等執(zhí)行完之后返回給用戶FILE*,讓用戶進行操控。所以我們也就可以理解維護在庫中了。
就像我們使用STL中的各種容器,不需要管底層是如何擴容的。
再次感受一下pthread_join():
所以我們也就理解了pthread_create時的attr
這就是用來控制pthread_t的屬性,比如控制棧的大小…
總結(jié):linux線程 = pthread庫 + LWP,其中內(nèi)核維護的LWP與動態(tài)庫中維護的線程是1:1
的。
但是同學們,我們該如何保障新線程輕量級進程會使用你指定的棧?
因為我們的輕量級進程中有一個系統(tǒng)調(diào)用:clone。
第一個是回調(diào)函數(shù),第二個就是指定的棧,第三個是參數(shù),所以pthread庫本質(zhì)就是對這樣的一堆系統(tǒng)調(diào)用進行封裝。
線程局部存儲:
現(xiàn)在還剩最后一個問題,線程局部存儲是啥?
我們先來看這樣一段代碼:
搞一個全局變量,新線程改,主線程讀取。
現(xiàn)象: 一改具改,符合預期。
但是如果我們想要互相不影響,也就是新主線程雖然用同一個全局變量名字,但是實際卻是兩個地址。
我們可以加入編譯選項__thread
注意:這個只可以修飾全局的內(nèi)置類型。
現(xiàn)象:可以看到虛擬地址也不相同。
結(jié)論:雖然看起來還是用一個,但實際上各自私有一份
封裝線程庫:
本質(zhì)是為了更好的理解C++11是如何進行封裝的。
我們的目標是實現(xiàn)如下幾個接口:
也可以在來一個GetStatus,調(diào)用可以觀察到此線程是否正在運行。
封裝:
先來解釋一下成員變量,_name就是線程名字,_tid是線程id,_func是用戶傳遞來的要執(zhí)行的函數(shù),_isRunning是代表當前線程是否在運行。
在實現(xiàn)時有兩個的坑點。
其一:因為我們是進行封裝,所以是在類中實現(xiàn)。
但是我們將routine寫出了之后卻找不到對應(yīng)routine,這是因為類中的成員默認有隱含的this指針
,所以參數(shù)的個數(shù)就不匹配了。
解決這種問題的辦法很多,但我們最喜歡用static進行修飾,這樣就不會有this指針了,這個函數(shù)屬于整個類。
可是這樣我們就無法訪問類中的成員變量了,因為沒有this指針~
那怎么辦?
把this指針當做參數(shù)傳給routine!
這樣就可以調(diào)用外部給我們傳的指定函數(shù)了。
可是這樣調(diào)用未免有些丑陋。
在將stop,join進行填補即可,注意,我們的構(gòu)造函數(shù)需要一個你指定的名字和待執(zhí)行函數(shù)。
主函數(shù)代碼:
現(xiàn)象:
也是完成了我們預期的工作。
源代碼:
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstdio>namespace cyc
{class mythread{public:typedef void (*func_t)(std::string);mythread(const std::string &name, func_t func) : _name(name), _func(func), _isRunning(false){}~mythread(){}void Excute(){_isRunning = true;_func(_name);_isRunning = false;}static void *routine(void *arg){mythread *self = static_cast<mythread *>(arg);self->Excute();return nullptr;}void Start(){int n = ::pthread_create(&_tid, nullptr, routine, (void *)this);if (n != 0){perror("pthread_create fail");exit(1);}}void Stop(){if (_isRunning){pthread_cancel(_tid);_isRunning = false;}}void Join(){int n = pthread_join(_tid, nullptr);if (n != 0){perror("pthread_join fail");exit(1);}}std::string GetStatus(){if(_isRunning) return "Running";else return "sleeping";}private:std::string _name;pthread_t _tid;func_t _func;bool _isRunning;};
}
于是我們就完成了一個很簡易的封裝~
互斥:
多個線程能夠看到的資源叫做共享資源。
但是也會有一些問題,比如我們線程通信,一個線程想寫hello world,但是剛寫了hello就被另一個進程獨走了,叫做讀寫不一致,
所以我們需要對共享資源做保護。
其中最簡單的方法是互斥。
但是我們總要先見一見吧。
我們模擬一個搶票的代碼,假設(shè)一共有1w張票,創(chuàng)建4哥線程同時去搶,每搶一次記錄一下?lián)屩暗钠睌?shù),當票數(shù)<=0就是出現(xiàn)了問題。
我們就是用剛剛模擬實現(xiàn)的線程進行操作。
代碼:
現(xiàn)象:
果然出現(xiàn)了0甚至負數(shù),這就證明我們的搶票提供非常的失敗~
可是原理是什么呢?
首先我們要有兩個儲備知識。
其一是判斷也是一種運算,為邏輯運算。
一共有兩種運算,分別為算術(shù)運算與邏輯運算,簡稱算羅運算。
其二是線程的切換,CPU內(nèi)寄存器只有一套,但是擁有的數(shù)據(jù)有多套。切換時帶走自己的數(shù)據(jù),回來時會回復!
對于1來說,邏輯判斷至少分為3步,雖然語法上表現(xiàn)為3步,但是實際轉(zhuǎn)換到匯編有2-3步。
我們假設(shè)線程1在還有最后一張票時進入,已經(jīng)判斷完畢了,但是此時tickets還沒進行--
,這時時間片到了,線程1被切換,帶走了當前寄存器的值與記錄執(zhí)行到的語句。同理2,3,4也都分別執(zhí)行完判斷語句就被切換這就有很大的問題了。
因為只有一張票卻有4個線程進入了,等線程a,b,c,d分別恢復時將數(shù)據(jù)又放回到寄存器中。
注意:進行--
時需要將數(shù)據(jù)重讀,修改,在放回內(nèi)存中這三步
所以票數(shù)就變?yōu)?,-1,-2…
下章繼續(xù)~