深入Golang調度器之GMP模型

      網友投稿 842 2025-03-31

      前言

      隨著服務器硬件迭代升級,配置也越來越高。為充分利用服務器資源,并發編程也變的越來越重要。在開始之前,需要了解一下并發(concurrency)和并行(parallesim)的區別。

      并發: 邏輯上具有處理多個同時性任務的能力。

      并行: 物理上同一時刻執行多個并發任務。

      通常所說的并發編程,也就是說它允許多個任務同時執行,但實際上并不一定在同一時刻被執行。在單核處理器上,通過多線程共享CPU時間片串行執行(并發非并行)。而并行則依賴于多核處理器等物理資源,讓多個任務可以實現并行執行(并發且并行)。

      多線程或多進程是并行的基本條件,但單線程也可以用協程(coroutine)做到并發。簡單將Goroutine歸納為協程并不合適,因為它運行時會創建多個線程來執行并發任務,且任務單元可被調度到其它線程執行。這更像是多線程和協程的結合體,能最大限度提升執行效率,發揮多核處理器能力。

      Go編寫一個并發編程程序很簡單,只需要在函數之前使用一個Go關鍵字就可以實現并發編程。

      func?main()?{????go?func(){ ????????fmt.Println("Hello,World!") ????}()}

      Go調度器組成

      Go語言雖然使用一個Go關鍵字即可實現并發編程,但Goroutine被調度到后端之后,具體的實現比較復雜。先看看調度器有哪幾部分組成。

      G是Goroutine的縮寫,相當于操作系統中的進程控制塊,在這里就是Goroutine的控制結構,是對Goroutine的抽象。其中包括執行的函數指令及參數;G保存的任務對象;線程上下文切換,現場保護和現場恢復需要的寄存器(SP、IP)等信息。

      Go不同版本Goroutine默認棧大小不同。

      //?Go1.11版本默認stack大小為2KB_StackMin?=?2048 ?//?創建一個g對象,然后放到g隊列//?等待被執行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_)???? ????if?newg?==?nil?{???????? ???????//?初始化g?stack大小 ????????newg?=?malg(_StackMin) ????????casgstatus(newg,?_Gidle,?_Gdead) ????????allgadd(newg) ????}???? ????//?以下省略}

      M是一個線程或稱為Machine,所有M是有線程棧的。如果不對該線程棧提供內存的話,系統會給該線程棧提供內存(不同操作系統提供的線程棧大小不同)。當指定了線程棧,則M.stack→G.stack,M的PC寄存器指向G提供的函數,然后去執行。

      深入Golang調度器之GMP模型

      type?m?struct?{???? ????/* ????????1.??所有調用棧的Goroutine,這是一個比較特殊的Goroutine。 ????????2.??普通的Goroutine棧是在Heap分配的可增長的stack,而g0的stack是M對應的線程棧。 ????????3.??所有調度相關代碼,會先切換到該Goroutine的棧再執行。 ????*/ ????g0???????*g ????curg?????*g?????????//?M當前綁定的結構體G ????//?SP、PC寄存器用于現場保護和現場恢復 ????vdsoSP?uintptr ????vdsoPC?uintptr ????//?省略…}

      P(Processor)是一個抽象的概念,并不是真正的物理CPU。所以當P有任務時需要創建或者喚醒一個系統線程來執行它隊列里的任務。所以P/M需要進行綁定,構成一個執行單元。

      P決定了同時可以并發任務的數量,可通過GOMAXPROCS限制同時執行用戶級任務的操作系統線程。可以通過runtime.GOMAXPROCS進行指定。在Go1.5之后GOMAXPROCS被默認設置可用的核數,而之前則默認為1。

      //?自定義設置GOMAXPROCS數量func?GOMAXPROCS(n?int)?int?{???? ????/* ????????1.??GOMAXPROCS設置可執行的CPU的最大數量,同時返回之前的設置。 ????????2.??如果n?>=?1 ????????} ????}???? ????if?n?==?0?{ ???????n?=?1 ????}???? ????return?n}//?一個進程默認被綁定在所有CPU核上,返回所有CPU?core。//?獲取進程的CPU親和性掩碼系統調用//?rax?204??????????????????????????;?系統調用碼//?system_call?sys_sched_getaffinity;?系統調用名稱//?rid??pid?????????????????????????;?進程號//?rsiunsignedint?len?????????????//?rdxunsignedlong?*user_mask_ptrsys_linux_amd64.s:TEXT?runtime·sched_getaffinity(SB),NOSPLIT,$0 ????MOVQ????pid+0(FP),?DI ????MOVQ????len+8(FP),?SI ????MOVQ????buf+16(FP),?DX ????MOVL????$SYS_sched_getaffinity,?AX ????SYSCALL ????MOVL????AX,?ret+24(FP) ????RET

      Go調度器調度過程

      首先創建一個G對象,G對象保存到P本地隊列或者是全局隊列。P此時去喚醒一個M。P繼續執行它的執行序。M尋找是否有空閑的P,如果有則將該G對象移動到它本身。接下來M執行一個調度循環(調用G對象->執行->清理線程→繼續找新的Goroutine執行)。

      M執行過程中,隨時會發生上下文切換。當發生上線文切換時,需要對執行現場進行保護,以便下次被調度執行時進行現場恢復。Go調度器M的棧保存在G對象上,只需要將M所需要的寄存器(SP、PC等)保存到G對象上就可以實現現場保護。當這些寄存器數據被保護起來,就隨時可以做上下文切換了,在中斷之前把現場保存起來。如果此時G任務還沒有執行完,M可以將任務重新丟到P的任務隊列,等待下一次被調度執行。當再次被調度執行時,M通過訪問G的vdsoSP、vdsoPC寄存器進行現場恢復(從上次中斷位置繼續執行)。

      通過上圖可以發現,P有兩種隊列:本地隊列和全局隊列。

      本地隊列: 當前P的隊列,本地隊列是Lock-Free,沒有數據競爭問題,無需加鎖處理,可以提升處理速度。

      全局隊列:全局隊列為了保證多個P之間任務的平衡。所有M共享P全局隊列,為保證數據競爭問題,需要加鎖處理。相比本地隊列處理速度要低于全局隊列。

      簡單理解為當時的環境即可,環境可以包括當時程序狀態以及變量狀態。例如線程切換的時候在內核會發生上下文切換,這里的上下文就包括了當時寄存器的值,把寄存器的值保存起來,等下次該線程又得到cpu時間的時候再恢復寄存器的值,這樣線程才能正確運行。

      對于代碼中某個值說,上下文是指這個值所在的局部(全局)作用域對象。相對于進程而言,上下文就是進程執行時的環境,具體來說就是各個變量和數據,包括所有的寄存器變量、進程打開的文件、內存(堆棧)信息等。

      Goroutine被調度執行必須保證P/M進行綁定,所以線程清理只需要將P釋放就可以實現線程的清理。什么時候P會釋放,保證其它G可以被執行。P被釋放主要有兩種情況。

      主動釋放:最典型的例子是,當執行G任務時有系統調用,當發生系統調用時M會處于Block狀態。調度器會設置一個超時時間,當超時時會將P釋放。

      被動釋放:如果發生系統調用,有一個專門監控程序,進行掃描當前處于阻塞的P/M組合。當超過系統程序設置的超時時間,會自動將P資源搶走。去執行隊列的其它G任務。

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

      重申一下重點:goroutine中的三個實體

      goroutine中最主要的是三個實體為GMP,其中:

      G: 代表一個goroutine對象,每次go調用的時候,都會創建一個G對象,它包括棧、指令指針以及對于調用goroutines很重要的其它信息,比如阻塞它的任何channel,其主要數據結構:

      type?g?struct?{ ??stack???????stack???//?描述了真實的棧內存,包括上下界 ??m??????????????*m?????//?當前的m ??sched??????????gobuf???//?goroutine切換時,用于保存g的上下文?????? ??param??????????unsafe.Pointer?//?用于傳遞參數,睡眠時其他goroutine可以設置param,喚醒時該goroutine可以獲取 ??atomicstatus???uint32 ??stackLock??????uint32? ??goid???????????int64??//?goroutine的ID ??waitsince??????int64?//?g被阻塞的大體時間 ??lockedm????????*m?????//?G被鎖定只在這個m上運行}

      其中最主要的當然是sched了,保存了goroutine的上下文。goroutine切換的時候不同于線程有OS來負責這部分數據,而是由一個gobuf對象來保存,這樣能夠更加輕量級,再來看看gobuf的結構:

      type?gobuf?struct?{ ????sp???uintptr ????pc???uintptr ????g????guintptr ????ctxt?unsafe.Pointer ????ret??sys.Uintreg ????lr???uintptr ????bp???uintptr?//?for?GOEXPERIMENT=framepointer}

      其實就是保存了當前的棧指針,計數器,當然還有g自身,這里記錄自身g的指針是為了能快速的訪問到goroutine中的信息。

      M:代表一個線程,每次創建一個M的時候,都會有一個底層線程創建;所有的G任務,最終還是在M上執行,其主要數據結構:

      type?m?struct?{ ????g0??????*g?????//?帶有調度棧的goroutine ????gsignal???????*g?????????//?處理信號的goroutine ????tls???????????[6]uintptr?//?thread-local?storage ????mstartfn??????func() ????curg??????????*g???????//?當前運行的goroutine ????caughtsig?????guintptr? ????p?????????????puintptr?//?關聯p和執行的go代碼 ????nextp?????????puintptr ????id????????????int32 ????mallocing?????int32?//?狀態 ????spinning??????bool?//?m是否out?of?work ????blocked???????bool?//?m是否被阻塞 ????inwb??????????bool?//?m是否在執行寫屏蔽 ????printlock?????int8 ????incgo?????????bool?//?m在執行cgo嗎 ????fastrand??????uint32 ????ncgocall??????uint64??????//?cgo調用的總數 ????ncgo??????????int32???????//?當前cgo調用的數目 ????park??????????note ????alllink???????*m?//?用于鏈接allm ????schedlink?????muintptr ????mcache????????*mcache?//?當前m的內存緩存 ????lockedg???????*g?//?鎖定g在當前m上執行,而不會切換到其他m ????createstack???[32]uintptr?//?thread創建的棧}

      結構體M中有兩個G是需要關注一下的,一個是curg,代表結構體M當前綁定的結構體G。另一個是g0,是帶有調度棧的goroutine,這是一個比較特殊的goroutine。普通的goroutine的棧是在堆上分配的可增長的棧,而g0的棧是M對應的線程的棧。所有調度相關的代碼,會先切換到該goroutine的棧中再執行。也就是說線程的棧也是用的g實現,而不是使用的OS的。

      P:代表一個處理器,每一個運行的M都必須綁定一個P,就像線程必須在么一個CPU核上執行一樣,由P來調度G在M上的運行,P的個數就是GOMAXPROCS(最大256),啟動時固定的,一般不修改;M的個數和P的個數不一定一樣多(會有休眠的M或者不需要太多的M)(最大10000);每一個P保存著本地G任務隊列,也有一個全局G任務隊列。P的數據結構:

      type?p?struct?{ ????lock?mutex ????id??????????int32 ????status??????uint32?//?狀態,可以為pidle/prunning/... ????link????????puintptr ????schedtick???uint32?????//?每調度一次加1 ????syscalltick?uint32?????//?每一次系統調用加1 ????sysmontick??sysmontick? ????m???????????muintptr???//?回鏈到關聯的m ????mcache??????*mcache ????racectx?????uintptr ????goidcache????uint64?//?goroutine的ID的緩存 ????goidcacheend?uint64 ????//?可運行的goroutine的隊列 ????runqhead?uint32 ????runqtail?uint32 ????runq?????[256]guintptr ????runnext?guintptr?//?下一個運行的g ????sudogcache?[]*sudog ????sudogbuf???[128]*sudog ????palloc?persistentAlloc?//?per-P?to?avoid?mutex ????pad?[sys.CacheLineSize]byte

      其中P的狀態有Pidle, Prunning, Psyscall, Pgcstop, Pdead;在其內部隊列runqhead里面有可運行的goroutine,P優先從內部獲取執行的g,這樣能夠提高效率。

      除此之外,還有一個數據結構需要在這里提及,就是schedt,可以看做是一個全局的調度者:

      type?schedt?struct?{ ???goidgen??uint64 ????lastpoll?uint64 ????lock?mutex ????midle????????muintptr?//?idle狀態的m ????nmidle???????int32????//?idle狀態的m個數 ????nmidlelocked?int32????//?lockde狀態的m個數 ????mcount???????int32????//?創建的m的總數 ????maxmcount????int32????//?m允許的最大個數 ????ngsys?uint32?//?系統中goroutine的數目,會自動更新 ????pidle??????puintptr?//?idle的p ????npidle?????uint32 ????nmspinning?uint32? ????//?全局的可運行的g隊列 ????runqhead?guintptr ????runqtail?guintptr ????runqsize?int32 ????//?dead的G的全局緩存 ????gflock???????mutex ????gfreeStack???*g ????gfreeNoStack?*g ????ngfree???????int32 ????//?sudog的緩存中心 ????sudoglock??mutex ????sudogcache?*sudog}

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

      goroutine的運行過程

      所有的goroutine都是由函數newproc來創建的,但是由于該函數不能調用分段棧,最后真正調用的是newproc1。在newproc1中主要進行如下動作:

      func?newproc1(fn?*funcval,?argp?*uint8,?narg?int32,?nret?int32,?callerpc?uintptr)?*g?{ ????newg?=?malg(_StackMin) ????casgstatus(newg,?_Gidle,?_Gdead) ????allgadd(newg)? ????newg.sched.sp?=?sp ????newg.stktopsp?=?sp ????newg.sched.pc?=?funcPC(goexit)?+?sys.PCQuantum? ????newg.sched.g?=?guintptr(unsafe.Pointer(newg)) ????gostartcallfn(&newg.sched,?fn) ????newg.gopc?=?callerpc ????newg.startpc?=?fn.fn????......}

      分配一個g的結構體

      初始化這個結構體的一些域

      將g掛在就緒隊列

      綁定g到一個m上

      這個綁定只要m沒有突破上限GOMAXPROCS,就拿一個m綁定一個g。如果m的waiting隊列中有就從隊列中拿,否則就要新建一個m,調用newm。

      func?newm(fn?func(),?_p_?*p)?{ ????mp?:=?allocm(_p_,?fn) ????mp.nextp.set(_p_) ????mp.sigmask?=?initSigmask ????execLock.rlock() ????newosproc(mp,?unsafe.Pointer(mp.g0.stack.hi)) ????execLock.runlock()}

      該函數其實就是創建一個m,跟newproc有些相似,之前也說了m在底層就是一個線程的創建,也即是newosproc函數,在往下挖可以看到會根據不同的OS來執行不同的bsdthread_create函數,而底層就是調用的runtime.clone:

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

      m創建好之后,線程的入口是mstart,最后調用的即是mstart1:

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

      里面最重要的就是schedule了,在schedule中的動作大體就是找到一個等待運行的g,然后然后搬到m上,設置其狀態為Grunning,直接切換到g的上下文環境,恢復g的執行。

      func?schedule()?{ ????_g_?:=?getg() ????if?_g_.m.lockedg?!=nil?{ ????????stoplockedm() ????????execute(_g_.m.lockedg,?false)?//?Never?returns.????}}

      schedule的執行可以大體總結為:

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

      簡單來說g所經歷的幾個主要的過程就是:Gwaiting->Grunnable->Grunning。經歷了創建,到掛在就緒隊列,到從就緒隊列拿出并運行整個過程。

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

      引入了struct M這層抽象。m就是這里的worker,但不是線程。處理系統調用中的m不會占用mcpu數量,只有干事的m才會對應到線程.當mcpu數量少于GOMAXPROCS時可以一直開新的線程干活.而goroutine的執行則是在m和g都滿足之后通過schedule切換上下文進入的.

      搶占式調度

      當有很多goroutine需要執行的時候,是怎么調度的了,上面說的P還沒有出場呢,在runtime.main中會創建一個額外m運行sysmon函數,搶占就是在sysmon中實現的。

      sysmon會進入一個無限循環, 第一輪回休眠20us, 之后每次休眠時間倍增, 最終每一輪都會休眠10ms. sysmon中有netpool(獲取fd事件),?retake(搶占), forcegc(按時間強制執行gc),?scavenge?heap(釋放自由列表中多余的項減少內存占用)等處理.

      func?sysmon()?{ ????lasttrace?:=?int64(0) ????idle?:=?0?//?how?many?cycles?in?succession?we?had?not?wokeup?somebody ????delay?:=?uint32(0) ????for?{ ????????if?idle?==?0?{?//?start?with?20us?sleep... ????????????delay?=?20 ????????}?else?if?idle?>?50?{?//?start?doubling?the?sleep?after?1ms... ????????????delay?*=?2 ????????} ????????if?delay?>?10*1000?{?//?up?to?10ms ????????????delay?=?10?*?1000 ????????} ????????usleep(delay) ????????...... ????}???????}

      里面的函數retake負責搶占:

      func?retake(now?int64)?uint32?{ ????n?:=?0 ????for?i?:=?int32(0);?i??0?&&?pd.syscallwhen+10*1000*1000?>?now?{ ????????????????continue ????????????} ????????????incidlelocked(-1) ????????????if?atomic.Cas(&_p_.status,?s,?_Pidle)?{ ????????????????if?trace.enabled?{ ????????????????????traceGoSysBlock(_p_) ????????????????????traceProcStop(_p_) ????????????????} ????????????????n++ ????????????????_p_.syscalltick++ ????????????????handoffp(_p_) ????????????} ????????????incidlelocked(1) ????????}?else?if?s?==?_Prunning?{ ????????????//?如果G運行時間過長,則搶占該G ????????????t?:=?int64(_p_.schedtick) ????????????if?int64(pd.schedtick)?!=?t?{ ????????????????pd.schedtick?=?uint32(t) ????????????????pd.schedwhen?=?now????????????????continue ????????????} ????????????if?pd.schedwhen+forcePreemptNS?>?now?{ ????????????????continue ????????????} ????????????preemptone(_p_) ????????} ????} ????return?uint32(n)}

      枚舉所有的P 如果P在系統調用中(_Psyscall), 且經過了一次sysmon循環(20us~10ms), 則搶占這個P, 調用handoffp解除M和P之間的關聯, 如果P在運行中(_Prunning), 且經過了一次sysmon循環并且G運行時間超過forcePreemptNS(10ms), 則搶占這個P

      并設置g.preempt?= true,g.stackguard0 = stackPreempt。

      為什么設置了stackguard就可以實現搶占?

      因為這個值用于檢查當前棧空間是否足夠, go函數的開頭會比對這個值判斷是否需要擴張棧。

      newstack函數判斷g.stackguard0等于stackPreempt, 就知道這是搶占觸發的, 這時會再檢查一遍是否要搶占。

      搶占機制保證了不會有一個G長時間的運行導致其他G無法運行的情況發生。

      總結

      相比大多數并行設計模型,Go比較優勢的設計就是P上下文這個概念的出現,如果只有G和M的對應關系,那么當G阻塞在IO上的時候,M是沒有實際在工作的,這樣造成了資源的浪費,沒有了P,那么所有G的列表都放在全局,這樣導致臨界區太大,對多核調度造成極大影響。

      而goroutine在使用上面的特點,感覺既可以用來做密集的多核計算,又可以做高并發的IO應用,做IO應用的時候,寫起來感覺和對程序員最友好的同步阻塞一樣,而實際上由于runtime的調度,底層是以同步非阻塞的方式在運行(即IO多路復用)。

      所以說保護現場的搶占式調度和G被阻塞后傳遞給其他m調用的核心思想,使得goroutine的產生。

      軟件開發

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:oa系統(oa系統哪個品牌好)
      下一篇:表格中乘法公式怎么輸(表格怎樣輸入乘法公式)
      相關文章
      亚洲国产另类久久久精品| 亚洲一区二区三区在线观看精品中文| 亚洲无码黄色网址| 亚洲熟妇av午夜无码不卡| 亚洲国产亚洲综合在线尤物| 亚洲视频在线观看免费视频| 亚洲资源在线观看| 久久亚洲精品无码AV红樱桃| 亚洲AV无码久久精品狠狠爱浪潮| 美腿丝袜亚洲综合| 国产成人亚洲精品狼色在线| 亚洲综合精品香蕉久久网| 国产亚洲大尺度无码无码专线| 亚洲最大av无码网址| 国产亚洲精品免费视频播放 | 国产精品亚洲精品日韩电影| 精品国产日韩亚洲一区91| 日日摸日日碰夜夜爽亚洲| 国产亚洲福利一区二区免费看| 国产综合成人亚洲区| 亚洲成A人片77777国产| 亚洲福利在线播放| 亚洲无码高清在线观看| 亚洲精品无码专区久久久| 亚洲av日韩综合一区在线观看| 亚洲AV永久无码精品一百度影院| 久久久久亚洲av无码专区蜜芽| 亚洲国产精品久久久久久| 亚洲一区综合在线播放| 亚洲男女性高爱潮网站| 亚洲av无码一区二区三区观看| 亚洲色图激情文学| 亚洲Aⅴ在线无码播放毛片一线天| 国产精品亚洲色婷婷99久久精品| 亚洲精品国自产拍在线观看| 亚洲精品亚洲人成人网| 99久久亚洲综合精品成人网| 亚洲xxxxxx| 日本亚洲欧美色视频在线播放| 亚洲国产中文字幕在线观看| 国产成人亚洲精品青草天美|