深入Golang調(diào)度器之GMP模型

Go語(yǔ)言雖然使用一個(gè)Go關(guān)鍵字即可實(shí)現(xiàn)并發(fā)編程,但Goroutine被調(diào)度到后端之后,具體的實(shí)現(xiàn)比較復(fù)雜。先看看調(diào)度器有哪幾部分組成。

1、G

G是Goroutine的縮寫,相當(dāng)于操作系統(tǒng)中的進(jìn)程控制塊,在這里就是Goroutine的控制結(jié)構(gòu),是對(duì)Goroutine的抽象。其中包括執(zhí)行的函數(shù)指令及參數(shù);G保存的任務(wù)對(duì)象;線程上下文切換,現(xiàn)場(chǎng)保護(hù)和現(xiàn)場(chǎng)恢復(fù)需要的寄存器(SP、IP)等信息。

Go不同版本Goroutine默認(rèn)棧大小不同。


// Go1.11版本默認(rèn)stack大小為2KB

_StackMin = 2048

// 創(chuàng)建一個(gè)g對(duì)象,然后放到g隊(duì)列

// 等待被執(zhí)行

func newproc1(fn *funcval, argp *uint8, narg int32, callergp *g, callerpc uintptr) {_g_ := getg() ? ?_g_.m.locks++ ? ?siz := narg ? ?siz = (siz +7) &^7_p_ := _g_.m.p.ptr() ? ?newg := gfget(_p_)

ifnewg ==nil{

// 初始化g stack大小newg = malg(_StackMin) ? ? ? ?casgstatus(newg, _Gidle, _Gdead) ? ? ? ?allgadd(newg) ? ?}

// 以下省略}


2、M

M是一個(gè)線程或稱為Machine,所有M是有線程棧的。如果不對(duì)該線程棧提供內(nèi)存的話,系統(tǒng)會(huì)給該線程棧提供內(nèi)存(不同操作系統(tǒng)提供的線程棧大小不同)。當(dāng)指定了線程棧,則M.stack→G.stack,M的PC寄存器指向G提供的函數(shù),然后去執(zhí)行。

type m struct { ? ?

/* ? ? ? ?1. ?所有調(diào)用棧的Goroutine,這是一個(gè)比較特殊的Goroutine。 ? ? ? ?2. ?普通的Goroutine棧是在Heap分配的可增長(zhǎng)的stack,而g0的stack是M對(duì)應(yīng)的線程棧。 ? ? ? ?3. ?所有調(diào)度相關(guān)代碼,會(huì)先切換到該Goroutine的棧再執(zhí)行。 ? ?*/g0*gcurg*g//M當(dāng)前綁定的結(jié)構(gòu)體G//SP、PC寄存器用于現(xiàn)場(chǎng)保護(hù)和現(xiàn)場(chǎng)恢復(fù)vdsoSPuintptrvdsoPCuintptr// 省略…}

3、P

P(Processor)是一個(gè)抽象的概念,并不是真正的物理CPU。所以當(dāng)P有任務(wù)時(shí)需要?jiǎng)?chuàng)建或者喚醒一個(gè)系統(tǒng)線程來(lái)執(zhí)行它隊(duì)列里的任務(wù)。所以P/M需要進(jìn)行綁定,構(gòu)成一個(gè)執(zhí)行單元。

P決定了同時(shí)可以并發(fā)任務(wù)的數(shù)量,可通過(guò)GOMAXPROCS限制同時(shí)執(zhí)行用戶級(jí)任務(wù)的操作系統(tǒng)線程。可以通過(guò)runtime.GOMAXPROCS進(jìn)行指定。在Go1.5之后GOMAXPROCS被默認(rèn)設(shè)置可用的核數(shù),而之前則默認(rèn)為1。

// 自定義設(shè)置GOMAXPROCS數(shù)量

func GOMAXPROCS(n int) int { ? ?

? ?/*1.GOMAXPROCS設(shè)置可執(zhí)行的CPU的最大數(shù)量,同時(shí)返回之前的設(shè)置。2.如果n <1,則不更改當(dāng)前的值。 ? ?*/ ? ?ret :=int(gomaxprocs) ? ?stopTheWorld("GOMAXPROCS")

// startTheWorld啟動(dòng)時(shí),使用newprocs。newprocs =int32(n) ? ?startTheWorld()

returnret}

// 默認(rèn)P被綁定到所有CPU核上

// P == cpu.cores

func getproccount() int32 { ? ?

? ?const maxCPUs = 64 * 1024varbuf [maxCPUs /8]byte// 獲取CPU Corer := sched_getaffinity(0, unsafe.Sizeof(buf), &buf[0]) ? ?n :=int32(0)

for_, v :=rangebuf[:r] {

forv !=0{ ? ? ? ? ? ?n +=int32(v &1) ? ? ? ? ? ?v >>=1} ? ?}

ifn ==0{ ? ? ? n =1}

returnn}

// 一個(gè)進(jìn)程默認(rèn)被綁定在所有CPU核上,返回所有CPU core。

// 獲取進(jìn)程的CPU親和性掩碼系統(tǒng)調(diào)用

// rax 204 ? ? ? ? ? ? ? ? ? ? ? ? ?; 系統(tǒng)調(diào)用碼

// system_call sys_sched_getaffinity; 系統(tǒng)調(diào)用名稱

// rid ?pid ? ? ? ? ? ? ? ? ? ? ? ? ; 進(jìn)程號(hào)

// rsi unsigned int len ? ? ? ? ? ?

// rdx unsigned long *user_mask_ptr

sys_linux_amd64.s:TEXT runtime·sched_getaffinity(SB),NOSPLIT,$0MOVQ ? ?pid+0(FP), DI ? ?MOVQlen+8(FP), SI ? ?MOVQ ? ?buf+16(FP), DX ? ?MOVL ? ?$SYS_sched_getaffinity, AX ? ?SYSCALL ? ?MOVL ? ?AX, ret+24(FP) ? ?RET

Go調(diào)度器調(diào)度過(guò)程

首先創(chuàng)建一個(gè)G對(duì)象,G對(duì)象保存到P本地隊(duì)列或者是全局隊(duì)列。P此時(shí)去喚醒一個(gè)M。P繼續(xù)執(zhí)行它的執(zhí)行序。M尋找是否有空閑的P,如果有則將該G對(duì)象移動(dòng)到它本身。接下來(lái)M執(zhí)行一個(gè)調(diào)度循環(huán)(調(diào)用G對(duì)象->執(zhí)行->清理線程→繼續(xù)找新的Goroutine執(zhí)行)。

M執(zhí)行過(guò)程中,隨時(shí)會(huì)發(fā)生上下文切換。當(dāng)發(fā)生上線文切換時(shí),需要對(duì)執(zhí)行現(xiàn)場(chǎng)進(jìn)行保護(hù),以便下次被調(diào)度執(zhí)行時(shí)進(jìn)行現(xiàn)場(chǎng)恢復(fù)。Go調(diào)度器M的棧保存在G對(duì)象上,只需要將M所需要的寄存器(SP、PC等)保存到G對(duì)象上就可以實(shí)現(xiàn)現(xiàn)場(chǎng)保護(hù)。當(dāng)這些寄存器數(shù)據(jù)被保護(hù)起來(lái),就隨時(shí)可以做上下文切換了,在中斷之前把現(xiàn)場(chǎng)保存起來(lái)。如果此時(shí)G任務(wù)還沒(méi)有執(zhí)行完,M可以將任務(wù)重新丟到P的任務(wù)隊(duì)列,等待下一次被調(diào)度執(zhí)行。當(dāng)再次被調(diào)度執(zhí)行時(shí),M通過(guò)訪問(wèn)G的vdsoSP、vdsoPC寄存器進(jìn)行現(xiàn)場(chǎng)恢復(fù)(從上次中斷位置繼續(xù)執(zhí)行)。


1、P 隊(duì)列

通過(guò)上圖可以發(fā)現(xiàn),P有兩種隊(duì)列:本地隊(duì)列和全局隊(duì)列。

本地隊(duì)列:當(dāng)前P的隊(duì)列,本地隊(duì)列是Lock-Free,沒(méi)有數(shù)據(jù)競(jìng)爭(zhēng)問(wèn)題,無(wú)需加鎖處理,可以提升處理速度。

全局隊(duì)列:全局隊(duì)列為了保證多個(gè)P之間任務(wù)的平衡。所有M共享P全局隊(duì)列,為保證數(shù)據(jù)競(jìng)爭(zhēng)問(wèn)題,需要加鎖處理。相比本地隊(duì)列處理速度要低于全局隊(duì)列。

2、上線文切換

簡(jiǎn)單理解為當(dāng)時(shí)的環(huán)境即可,環(huán)境可以包括當(dāng)時(shí)程序狀態(tài)以及變量狀態(tài)。例如線程切換的時(shí)候在內(nèi)核會(huì)發(fā)生上下文切換,這里的上下文就包括了當(dāng)時(shí)寄存器的值,把寄存器的值保存起來(lái),等下次該線程又得到cpu時(shí)間的時(shí)候再恢復(fù)寄存器的值,這樣線程才能正確運(yùn)行。

對(duì)于代碼中某個(gè)值說(shuō),上下文是指這個(gè)值所在的局部(全局)作用域?qū)ο蟆O鄬?duì)于進(jìn)程而言,上下文就是進(jìn)程執(zhí)行時(shí)的環(huán)境,具體來(lái)說(shuō)就是各個(gè)變量和數(shù)據(jù),包括所有的寄存器變量、進(jìn)程打開(kāi)的文件、內(nèi)存(堆棧)信息等。

3、線程清理

Goroutine被調(diào)度執(zhí)行必須保證P/M進(jìn)行綁定,所以線程清理只需要將P釋放就可以實(shí)現(xiàn)線程的清理。什么時(shí)候P會(huì)釋放,保證其它G可以被執(zhí)行。P被釋放主要有兩種情況。

主動(dòng)釋放:最典型的例子是,當(dāng)執(zhí)行G任務(wù)時(shí)有系統(tǒng)調(diào)用,當(dāng)發(fā)生系統(tǒng)調(diào)用時(shí)M會(huì)處于Block狀態(tài)。調(diào)度器會(huì)設(shè)置一個(gè)超時(shí)時(shí)間,當(dāng)超時(shí)時(shí)會(huì)將P釋放。

被動(dòng)釋放:如果發(fā)生系統(tǒng)調(diào)用,有一個(gè)專門監(jiān)控程序,進(jìn)行掃描當(dāng)前處于阻塞的P/M組合。當(dāng)超過(guò)系統(tǒng)程序設(shè)置的超時(shí)時(shí)間,會(huì)自動(dòng)將P資源搶走。去執(zhí)行隊(duì)列的其它G任務(wù)。

終于要來(lái)說(shuō)說(shuō)Golang中最吸引人的goroutine了,這也是Golang能夠橫空出世的主要原因。不同于Python基于進(jìn)程的并發(fā)模型,以及C++、Java等基于線程的并發(fā)模型。Golang采用輕量級(jí)的goroutine來(lái)實(shí)現(xiàn)并發(fā),可以大大減少CPU的切換。現(xiàn)在已經(jīng)有太多的文章來(lái)介紹goroutine的用法,在這里,我們從源碼的角度來(lái)看看其內(nèi)部實(shí)現(xiàn)。

重申一下重點(diǎn):goroutine中的三個(gè)實(shí)體

goroutine中最主要的是三個(gè)實(shí)體為GMP,其中:

G:代表一個(gè)goroutine對(duì)象,每次go調(diào)用的時(shí)候,都會(huì)創(chuàng)建一個(gè)G對(duì)象,它包括棧、指令指針以及對(duì)于調(diào)用goroutines很重要的其它信息,比如阻塞它的任何channel,其主要數(shù)據(jù)結(jié)構(gòu):

typegstruct{stackstack// 描述了真實(shí)的棧內(nèi)存,包括上下界m*m// 當(dāng)前的mschedgobuf// goroutine切換時(shí),用于保存g的上下文paramunsafe.Pointer// 用于傳遞參數(shù),睡眠時(shí)其他goroutine可以設(shè)置param,喚醒時(shí)該goroutine可以獲取atomicstatusuint32stackLockuint32goidint64// goroutine的IDwaitsinceint64// g被阻塞的大體時(shí)間lockedm*m// G被鎖定只在這個(gè)m上運(yùn)行}

其中最主要的當(dāng)然是sched了,保存了goroutine的上下文。goroutine切換的時(shí)候不同于線程有OS來(lái)負(fù)責(zé)這部分?jǐn)?shù)據(jù),而是由一個(gè)gobuf對(duì)象來(lái)保存,這樣能夠更加輕量級(jí),再來(lái)看看gobuf的結(jié)構(gòu):

typegobufstruct{spuintptrpcuintptrgguintptrctxtunsafe.Pointerretsys.Uintreglruintptrbpuintptr// for GOEXPERIMENT=framepointer}

其實(shí)就是保存了當(dāng)前的棧指針,計(jì)數(shù)器,當(dāng)然還有g(shù)自身,這里記錄自身g的指針是為了能快速的訪問(wèn)到goroutine中的信息。

M:代表一個(gè)線程,每次創(chuàng)建一個(gè)M的時(shí)候,都會(huì)有一個(gè)底層線程創(chuàng)建;所有的G任務(wù),最終還是在M上執(zhí)行,其主要數(shù)據(jù)結(jié)構(gòu):

typemstruct{g0*g// 帶有調(diào)度棧的goroutinegsignal*g// 處理信號(hào)的goroutinetls[6]uintptr// thread-local storagemstartfnfunc()curg*g// 當(dāng)前運(yùn)行的goroutinecaughtsigguintptrppuintptr// 關(guān)聯(lián)p和執(zhí)行的go代碼nextppuintptridint32mallocingint32// 狀態(tài)spinningbool// m是否out of workblockedbool// m是否被阻塞inwbbool// m是否在執(zhí)行寫屏蔽printlockint8incgobool// m在執(zhí)行cgo嗎fastranduint32ncgocalluint64// cgo調(diào)用的總數(shù)ncgoint32// 當(dāng)前cgo調(diào)用的數(shù)目parknotealllink*m// 用于鏈接allmschedlinkmuintptrmcache*mcache// 當(dāng)前m的內(nèi)存緩存lockedg*g// 鎖定g在當(dāng)前m上執(zhí)行,而不會(huì)切換到其他mcreatestack[32]uintptr// thread創(chuàng)建的棧}

結(jié)構(gòu)體M中有兩個(gè)G是需要關(guān)注一下的,一個(gè)是curg,代表結(jié)構(gòu)體M當(dāng)前綁定的結(jié)構(gòu)體G。另一個(gè)是g0,是帶有調(diào)度棧的goroutine,這是一個(gè)比較特殊的goroutine。普通的goroutine的棧是在堆上分配的可增長(zhǎng)的棧,而g0的棧是M對(duì)應(yīng)的線程的棧。所有調(diào)度相關(guān)的代碼,會(huì)先切換到該goroutine的棧中再執(zhí)行。也就是說(shuō)線程的棧也是用的g實(shí)現(xiàn),而不是使用的OS的。

P:代表一個(gè)處理器,每一個(gè)運(yùn)行的M都必須綁定一個(gè)P,就像線程必須在么一個(gè)CPU核上執(zhí)行一樣,由P來(lái)調(diào)度G在M上的運(yùn)行,P的個(gè)數(shù)就是GOMAXPROCS(最大256),啟動(dòng)時(shí)固定的,一般不修改;M的個(gè)數(shù)和P的個(gè)數(shù)不一定一樣多(會(huì)有休眠的M或者不需要太多的M)(最大10000);每一個(gè)P保存著本地G任務(wù)隊(duì)列,也有一個(gè)全局G任務(wù)隊(duì)列。P的數(shù)據(jù)結(jié)構(gòu):

typepstruct{lockmutexidint32statusuint32// 狀態(tài),可以為pidle/prunning/...linkpuintptrschedtickuint32// 每調(diào)度一次加1syscalltickuint32// 每一次系統(tǒng)調(diào)用加1sysmonticksysmontickmmuintptr// 回鏈到關(guān)聯(lián)的mmcache*mcacheracectxuintptrgoidcacheuint64// goroutine的ID的緩存goidcacheenduint64// 可運(yùn)行的goroutine的隊(duì)列runqheaduint32runqtailuint32runq[256]guintptrrunnextguintptr// 下一個(gè)運(yùn)行的gsudogcache[]*sudogsudogbuf[128]*sudogpallocpersistentAlloc// per-P to avoid mutexpad[sys.CacheLineSize]byte

其中P的狀態(tài)有Pidle, Prunning, Psyscall, Pgcstop, Pdead;在其內(nèi)部隊(duì)列runqhead里面有可運(yùn)行的goroutine,P優(yōu)先從內(nèi)部獲取執(zhí)行的g,這樣能夠提高效率。

除此之外,還有一個(gè)數(shù)據(jù)結(jié)構(gòu)需要在這里提及,就是schedt,可以看做是一個(gè)全局的調(diào)度者:

typeschedtstruct{goidgenuint64lastpolluint64lockmutexmidlemuintptr// idle狀態(tài)的mnmidleint32// idle狀態(tài)的m個(gè)數(shù)nmidlelockedint32// lockde狀態(tài)的m個(gè)數(shù)mcountint32// 創(chuàng)建的m的總數(shù)maxmcountint32// m允許的最大個(gè)數(shù)ngsysuint32// 系統(tǒng)中g(shù)oroutine的數(shù)目,會(huì)自動(dòng)更新pidlepuintptr// idle的pnpidleuint32nmspinninguint32// 全局的可運(yùn)行的g隊(duì)列runqheadguintptrrunqtailguintptrrunqsizeint32// dead的G的全局緩存gflockmutexgfreeStack*ggfreeNoStack*gngfreeint32// sudog的緩存中心sudoglockmutexsudogcache*sudog}

大多數(shù)需要的信息都已放在了結(jié)構(gòu)體M、G和P中,schedt結(jié)構(gòu)體只是一個(gè)殼。可以看到,其中有M的idle隊(duì)列,P的idle隊(duì)列,以及一個(gè)全局的就緒的G隊(duì)列。schedt結(jié)構(gòu)體中的Lock是非常必須的,如果M或P等做一些非局部的操作,它們一般需要先鎖住調(diào)度器。

goroutine的運(yùn)行過(guò)程

所有的goroutine都是由函數(shù)newproc來(lái)創(chuàng)建的,但是由于該函數(shù)不能調(diào)用分段棧,最后真正調(diào)用的是newproc1。在newproc1中主要進(jìn)行如下動(dòng)作:

funcnewproc1(fn*funcval,argp*uint8,nargint32,nretint32,callerpcuintptr)*g{newg=malg(_StackMin)casgstatus(newg,_Gidle,_Gdead)allgadd(newg)newg.sched.sp=spnewg.stktopsp=spnewg.sched.pc=funcPC(goexit)+sys.PCQuantumnewg.sched.g=guintptr(unsafe.Pointer(newg))gostartcallfn(&newg.sched,fn)newg.gopc=callerpcnewg.startpc=fn.fn......}

分配一個(gè)g的結(jié)構(gòu)體

初始化這個(gè)結(jié)構(gòu)體的一些域

將g掛在就緒隊(duì)列

綁定g到一個(gè)m上

這個(gè)綁定只要m沒(méi)有突破上限GOMAXPROCS,就拿一個(gè)m綁定一個(gè)g。如果m的waiting隊(duì)列中有就從隊(duì)列中拿,否則就要新建一個(gè)m,調(diào)用newm。

funcnewm(fnfunc(),_p_*p){mp:=allocm(_p_,fn)mp.nextp.set(_p_)mp.sigmask=initSigmaskexecLock.rlock()newosproc(mp,unsafe.Pointer(mp.g0.stack.hi))execLock.runlock()}

該函數(shù)其實(shí)就是創(chuàng)建一個(gè)m,跟newproc有些相似,之前也說(shuō)了m在底層就是一個(gè)線程的創(chuàng)建,也即是newosproc函數(shù),在往下挖可以看到會(huì)根據(jù)不同的OS來(lái)執(zhí)行不同的bsdthread_create函數(shù),而底層就是調(diào)用的runtime.clone:

clone(cloneFlags,stk,unsafe.Pointer(mp),unsafe.Pointer(mp.g0),unsafe.Pointer(funcPC(mstart)))

m創(chuàng)建好之后,線程的入口是mstart,最后調(diào)用的即是mstart1:

funcmstart1(){_g_:=getg()gosave(&_g_.m.g0.sched)_g_.m.g0.sched.pc=^uintptr(0)asminit()minit()if_g_.m==&m0{initsig(false)}iffn:=_g_.m.mstartfn;fn!=nil{fn()}schedule()}

里面最重要的就是schedule了,在schedule中的動(dòng)作大體就是找到一個(gè)等待運(yùn)行的g,然后然后搬到m上,設(shè)置其狀態(tài)為Grunning,直接切換到g的上下文環(huán)境,恢復(fù)g的執(zhí)行。

funcschedule(){_g_:=getg()if_g_.m.lockedg!=nil{stoplockedm()execute(_g_.m.lockedg,false)// Never returns.}}

schedule的執(zhí)行可以大體總結(jié)為:

schedule函數(shù)獲取g => [必要時(shí)休眠] => [喚醒后繼續(xù)獲取] => execute函數(shù)執(zhí)行g(shù) => 執(zhí)行后返回到goexit => 重新執(zhí)行schedule函數(shù)

簡(jiǎn)單來(lái)說(shuō)g所經(jīng)歷的幾個(gè)主要的過(guò)程就是:Gwaiting->Grunnable->Grunning。經(jīng)歷了創(chuàng)建,到掛在就緒隊(duì)列,到從就緒隊(duì)列拿出并運(yùn)行整個(gè)過(guò)程。

casgstatus(gp,_Gwaiting,_Grunnable)casgstatus(gp,_Grunnable,_Grunning)

引入了struct M這層抽象。m就是這里的worker,但不是線程。處理系統(tǒng)調(diào)用中的m不會(huì)占用mcpu數(shù)量,只有干事的m才會(huì)對(duì)應(yīng)到線程.當(dāng)mcpu數(shù)量少于GOMAXPROCS時(shí)可以一直開(kāi)新的線程干活.而goroutine的執(zhí)行則是在m和g都滿足之后通過(guò)schedule切換上下文進(jìn)入的.

搶占式調(diào)度

當(dāng)有很多goroutine需要執(zhí)行的時(shí)候,是怎么調(diào)度的了,上面說(shuō)的P還沒(méi)有出場(chǎng)呢,在runtime.main中會(huì)創(chuàng)建一個(gè)額外m運(yùn)行sysmon函數(shù),搶占就是在sysmon中實(shí)現(xiàn)的。

sysmon會(huì)進(jìn)入一個(gè)無(wú)限循環(huán), 第一輪回休眠20us, 之后每次休眠時(shí)間倍增, 最終每一輪都會(huì)休眠10ms. sysmon中有netpool(獲取fd事件), retake(搶占), forcegc(按時(shí)間強(qiáng)制執(zhí)行g(shù)c), scavenge heap(釋放自由列表中多余的項(xiàng)減少內(nèi)存占用)等處理.

funcsysmon(){lasttrace:=int64(0)idle:=0// how many cycles in succession we had not wokeup somebodydelay:=uint32(0)for{ifidle==0{// start with 20us sleep...delay=20}elseifidle>50{// start doubling the sleep after 1ms...delay*=2}ifdelay>10*1000{// up to 10msdelay=10*1000}usleep(delay)......}}

里面的函數(shù)retake負(fù)責(zé)搶占:

funcretake(nowint64)uint32{n:=0fori:=int32(0);i0&&pd.syscallwhen+10*1000*1000>now{continue}incidlelocked(-1)ifatomic.Cas(&_p_.status,s,_Pidle){iftrace.enabled{traceGoSysBlock(_p_)traceProcStop(_p_)}n++_p_.syscalltick++handoffp(_p_)}incidlelocked(1)}elseifs==_Prunning{// 如果G運(yùn)行時(shí)間過(guò)長(zhǎng),則搶占該Gt:=int64(_p_.schedtick)ifint64(pd.schedtick)!=t{pd.schedtick=uint32(t)pd.schedwhen=nowcontinue}ifpd.schedwhen+forcePreemptNS>now{continue}preemptone(_p_)}}returnuint32(n)}

枚舉所有的P 如果P在系統(tǒng)調(diào)用中(_Psyscall), 且經(jīng)過(guò)了一次sysmon循環(huán)(20us~10ms), 則搶占這個(gè)P, 調(diào)用handoffp解除M和P之間的關(guān)聯(lián), 如果P在運(yùn)行中(_Prunning), 且經(jīng)過(guò)了一次sysmon循環(huán)并且G運(yùn)行時(shí)間超過(guò)forcePreemptNS(10ms), 則搶占這個(gè)P

并設(shè)置g.preempt = true,g.stackguard0 = stackPreempt。

為什么設(shè)置了stackguard就可以實(shí)現(xiàn)搶占?

因?yàn)檫@個(gè)值用于檢查當(dāng)前棧空間是否足夠, go函數(shù)的開(kāi)頭會(huì)比對(duì)這個(gè)值判斷是否需要擴(kuò)張棧。

newstack函數(shù)判斷g.stackguard0等于stackPreempt, 就知道這是搶占觸發(fā)的, 這時(shí)會(huì)再檢查一遍是否要搶占。

搶占機(jī)制保證了不會(huì)有一個(gè)G長(zhǎng)時(shí)間的運(yùn)行導(dǎo)致其他G無(wú)法運(yùn)行的情況發(fā)生。

總結(jié)

相比大多數(shù)并行設(shè)計(jì)模型,Go比較優(yōu)勢(shì)的設(shè)計(jì)就是P上下文這個(gè)概念的出現(xiàn),如果只有G和M的對(duì)應(yīng)關(guān)系,那么當(dāng)G阻塞在IO上的時(shí)候,M是沒(méi)有實(shí)際在工作的,這樣造成了資源的浪費(fèi),沒(méi)有了P,那么所有G的列表都放在全局,這樣導(dǎo)致臨界區(qū)太大,對(duì)多核調(diào)度造成極大影響。

而goroutine在使用上面的特點(diǎn),感覺(jué)既可以用來(lái)做密集的多核計(jì)算,又可以做高并發(fā)的IO應(yīng)用,做IO應(yīng)用的時(shí)候,寫起來(lái)感覺(jué)和對(duì)程序員最友好的同步阻塞一樣,而實(shí)際上由于runtime的調(diào)度,底層是以同步非阻塞的方式在運(yùn)行(即IO多路復(fù)用)。

所以說(shuō)保護(hù)現(xiàn)場(chǎng)的搶占式調(diào)度和G被阻塞后傳遞給其他m調(diào)用的核心思想,使得goroutine的產(chǎn)生。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。