前言
其實這個話題我早就想做了,奈何這個問題確實有點復(fù)雜,看了很多文章才有了一點點自己的理解。從golang一開始的使用我就已經(jīng)開始好奇了,這個goroutine到底是怎么實現(xiàn)的呢坷衍?怎么就能搞出一個和線程類似,但是性能又那么好的東西的呢条舔?
模型
三個小伙子
在看整體結(jié)構(gòu)之前枫耳,我先來介紹三個小伙子,golang為了實現(xiàn)goroutine逞刷,定義了這樣三個小伙子嘉涌,讓他們幫忙去實現(xiàn)妻熊。
G
表示goroutine,存儲了goroutine的執(zhí)行stack信息仑最、goroutine狀態(tài)以及goroutine的任務(wù)函數(shù)等扔役;另外G對象是可以重用的。
M
M代表著真正的執(zhí)行計算資源警医。在綁定有效的P后亿胸,進入調(diào)度器循環(huán);而調(diào)度器循環(huán)的機制大致是從各種隊列预皇、P的本地隊列中獲取G侈玄,切換到G的執(zhí)行棧上并執(zhí)行G的函數(shù),調(diào)用goexit做清理工作并回到M吟温,如此反復(fù)序仙。M并不保留G狀態(tài),這是G可以跨M調(diào)度的基礎(chǔ)鲁豪。
P
表示邏輯processor潘悼,P的數(shù)量決定了系統(tǒng)內(nèi)最大可并行的G的數(shù)量(前提:系統(tǒng)的物理cpu核數(shù)>=P的數(shù)量);P的最大作用還是其擁有的各種G對象隊列爬橡、鏈表治唤、一些cache和狀態(tài)。
整體結(jié)構(gòu)模型
我們先來看下面的這張圖糙申,從大體結(jié)構(gòu)上看宾添,我們就能理解goroutine了
看到這個圖你應(yīng)該理解了一半,下面我來說明一下柜裸。
我們知道缕陕,在操作系統(tǒng)眼里其實只有cpu和線程,它去控制著各個線程的調(diào)度粘室,切換榄檬,執(zhí)行等等卜范,而對于goroutine的實現(xiàn)其實非常類似衔统;
在golang層面,首先我們要知道海雪,最終在外面執(zhí)行的肯定是線程锦爵,但是我們內(nèi)部開出的那些goroutine到哪里去了呢?
golang提出GPM模型奥裸,在G的眼里险掀,只有P,P保存了需要執(zhí)行的那些goroutine湾宙;
而在整個go調(diào)度的層面樟氢,對外的是M冈绊,P會找到一個M,讓它去與外面的線程交互埠啃,從而去真正執(zhí)行程序死宣。
從這里我們可以發(fā)現(xiàn),其實goroutine的調(diào)度器整個就是一個小型的操作系統(tǒng)碴开,內(nèi)部去造出了類似的部件去完成goroutine的實現(xiàn)毅该,而因為是在內(nèi)部實現(xiàn),所以解決了操作系統(tǒng)層面所帶來的線程創(chuàng)建慢等問題潦牛。
但是眶掌,同時這樣的調(diào)度也會有問題,所以需要一些額外的措施巴碗!
調(diào)度中的問題
問題1 G不均
我們知道朴爬,現(xiàn)實情況有的goroutine運行的快,有的慢橡淆,那么勢必肯定會帶來的問題就是寝殴,忙的忙死,閑的閑死明垢,go肯定不允許摸魚的P存在蚣常,勢必要榨干所有勞動力。
如果你沒有任務(wù)痊银,那么抵蚊,我們看到模型中還有一個全局G隊列的存在,如本地隊列已滿溯革,會一次性轉(zhuǎn)移半數(shù)到全局隊列贞绳。其他閑的小伙子就會從全局隊列中拿;(順便說一下致稀,優(yōu)先級是先拿下一個需要執(zhí)行的冈闭,然后去本地隊列中拿,再去全局隊列中拿抖单,先把自己的做完再做別人的嘛)同時如果全局都沒有了萎攒,就會去搶別人的做。
問題2 任務(wù)卡主了
萬一有個程序員啟動一個goroutine去執(zhí)行一個任務(wù)矛绘,然后這個任務(wù)一直睡覺(sleep)就是循環(huán)睡覺耍休,那咋辦嘛!你作為執(zhí)行人货矮,你總不能說羊精,讓它一直占用著一整個線程的資源,然后后面的goroutine都卡主了囚玫,那如果只有一個核心P喧锦,不就完蛋了读规?聰明的go才不會那么傻,它采用了搶占式調(diào)度來解決這個問題燃少。只要你這個任務(wù)執(zhí)行超過一定的時間(10ms)掖桦,那么這個任務(wù)就會被標(biāo)識為可搶占的,那么別的goroutine就可以搶先進來執(zhí)行供汛。只要下次這個goroutine進行函數(shù)調(diào)用枪汪,那么就會被強占,同時也會保護現(xiàn)場怔昨,然后重新放入P的本地隊列里面等待下次執(zhí)行雀久。
誰來做的呢?
sysmon趁舀,就是這個背后默默付出的人赖捌,它是一個后臺運行的監(jiān)控線程,它來監(jiān)控那些長時間運行的G任務(wù)然后設(shè)置可以強占的標(biāo)識符矮烹。(同時順便提一下越庇,它還會做的一些事情,例如奉狈,釋放閑置的span內(nèi)存卤唉,2分鐘的默認(rèn)gc等)
問題3 阻塞可怎么辦?
我們經(jīng)常使用goroutine還有一個場景就是網(wǎng)絡(luò)請求和IO操作仁期,這種阻塞的情況下桑驱,我們的G和M又會怎么做呢?
這個時候有個叫做netpoller的東西出現(xiàn)了跛蛋,當(dāng)每次有一個網(wǎng)絡(luò)請求阻塞的時候熬的,如果按照原來的方法這個時候這個請求會阻塞線程,而有了netpoller這個東西赊级,可以將請求阻塞到goroutine押框。
意思是說,當(dāng)阻塞出現(xiàn)的時候理逊,當(dāng)前goroutine會被阻塞橡伞,等待阻塞notify,而放出M去做別的g挡鞍,而當(dāng)阻塞恢復(fù)的時候骑歹,netpoller就會通知對應(yīng)的m可以做原來的g了。
同時還要順便提一句墨微,當(dāng)P發(fā)現(xiàn)沒有任務(wù)的時候,除了會找本地和全局扁掸,也會去netpoll中找翘县。
問題4 系統(tǒng)方法調(diào)用阻塞最域?
還有一個問題,我們自己想可能比較難想到锈麸,就是當(dāng)調(diào)用一些系統(tǒng)方法的時候镀脂,如果系統(tǒng)方法調(diào)用的時候發(fā)生阻塞就比較麻煩了。下面引用一段話:
當(dāng)G被阻塞在某個系統(tǒng)調(diào)用上時忘伞,此時G會阻塞在_Gsyscall狀態(tài)薄翅,M也處于block on syscall狀態(tài),此時的M可被搶占調(diào)度:執(zhí)行該G的M會與P解綁氓奈,而P則嘗試與其它idle的M綁定翘魄,繼續(xù)執(zhí)行其它G。如果沒有其它idle的M舀奶,但P的Local隊列中仍然有G需要執(zhí)行暑竟,則創(chuàng)建一個新的M;當(dāng)系統(tǒng)調(diào)用完成后育勺,G會重新嘗試獲取一個idle的P進入它的Local隊列恢復(fù)執(zhí)行但荤,如果沒有idle的P,G會被標(biāo)記為runnable加入到Global隊列涧至。
源碼一瞥
struct G
{
uintptr stackguard; // 分段棧的可用空間下界
uintptr stackbase; // 分段棧的椄乖辏基址
Gobuf sched; //進程切換時,利用sched域來保存上下文
uintptr stack0;
FuncVal* fnstart; // goroutine運行的函數(shù)
void* param; // 用于傳遞參數(shù)南蓬,睡眠時其它goroutine設(shè)置param潜慎,喚醒時此goroutine可以獲取
int16 status; // 狀態(tài)Gidle,Grunnable,Grunning,Gsyscall,Gwaiting,Gdead
int64 goid; // goroutine的id號
G* schedlink;
M* m; // for debuggers, but offset not hard-coded
M* lockedm; // G被鎖定只能在這個m上運行
uintptr gopc; // 創(chuàng)建這個goroutine的go表達式的pc
...
};
struct M
{
G* g0; // 帶有調(diào)度棧的goroutine
G* gsignal; // signal-handling G 處理信號的goroutine
void (*mstartfn)(void);
G* curg; // M中當(dāng)前運行的goroutine
P* p; // 關(guān)聯(lián)P以執(zhí)行Go代碼 (如果沒有執(zhí)行Go代碼則P為nil)
P* nextp;
int32 id;
int32 mallocing; //狀態(tài)
int32 throwing;
int32 gcing;
int32 locks;
int32 helpgc; //不為0表示此m在做幫忙gc。helpgc等于n只是一個編號
bool blockingsyscall;
bool spinning;
Note park;
M* alllink; // 這個域用于鏈接allm
M* schedlink;
MCache *mcache;
G* lockedg;
M* nextwaitm; // next M waiting for lock
GCStats gcstats;
...
};
struct P
{
Lock;
uint32 status; // Pidle或Prunning等
P* link;
uint32 schedtick; // 每次調(diào)度時將它加一
M* m; // 鏈接到它關(guān)聯(lián)的M (nil if idle)
MCache* mcache;
G* runq[256];
int32 runqhead;
int32 runqtail;
// Available G's (status == Gdead)
G* gfree;
int32 gfreecnt;
byte pad[64];
};
struct Sched {
Lock;
uint64 goidgen;
M* midle; // idle m's waiting for work
int32 nmidle; // number of idle m's waiting for work
int32 nmidlelocked; // number of locked m's waiting for work
int3 mcount; // number of m's that have been created
int32 maxmcount; // maximum number of m's allowed (or die)
P* pidle; // idle P's
uint32 npidle; //idle P的數(shù)量
uint32 nmspinning;
// Global runnable queue.
G* runqhead;
G* runqtail;
int32 runqsize;
// Global cache of dead G's.
Lock gflock;
G* gfree;
int32 stopwait;
Note stopnote;
uint32 sysmonwait;
Note sysmonnote;
uint64 lastpoll;
int32 profilehz; // cpu profiling rate
}
總結(jié)
goroutine的設(shè)計總的來說就是參考操作系統(tǒng)的設(shè)計蓖康,所有的目的很明確就是為了在整個運行過程中能充分利用已有的資源铐炫,盡可能在有限的資源里面多做事情,利用gpm的模型以及一些netpoller蒜焊、sysmon等幫助在阻塞的時候也能合理利用資源倒信,從而達到我們現(xiàn)在高效的goroutine
參考資料:
https://tiancaiamao.gitbooks.io/go-internals/content/zh/05.1.html
http://morsmachine.dk/go-scheduler
http://morsmachine.dk/netpoller
https://studygolang.com/articles/10116