這篇文章主要介紹“分析Go協(xié)作與搶占”,在日常操作中,相信很多人在分析Go協(xié)作與搶占問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對(duì)大家解答”分析Go協(xié)作與搶占”的疑惑有所幫助!接下來,請(qǐng)跟著小編一起來學(xué)習(xí)吧!
成都創(chuàng)新互聯(lián)-專業(yè)網(wǎng)站定制、快速模板網(wǎng)站建設(shè)、高性價(jià)比柳河網(wǎng)站開發(fā)、企業(yè)建站全套包干低至880元,成熟完善的模板庫,直接使用。一站式柳河網(wǎng)站制作公司更省心,省錢,快速模板網(wǎng)站建設(shè)找我們,業(yè)務(wù)覆蓋柳河地區(qū)。費(fèi)用合理售后完善,10余年實(shí)體公司更值得信賴。
協(xié)作式調(diào)度
主動(dòng)用戶讓權(quán):Gosched
Gosched 是一種主動(dòng)放棄執(zhí)行的手段,用戶態(tài)代碼通過調(diào)用此接口來出讓執(zhí)行機(jī)會(huì),使其他“人”也能在密集的執(zhí)行過程中獲得被調(diào)度的機(jī)會(huì)。
Gosched 的實(shí)現(xiàn)非常簡單:
// Gosched 會(huì)讓出當(dāng)前的 P,并允許其他 Goroutine 運(yùn)行。 // 它不會(huì)推遲當(dāng)前的 Goroutine,因此執(zhí)行會(huì)被自動(dòng)恢復(fù) func Gosched() { checkTimeouts() mcall(gosched_m) } // Gosched 在 g0 上繼續(xù)執(zhí)行 func gosched_m(gp *g) { ... goschedImpl(gp) }
它首先會(huì)通過 note 機(jī)制通知那些等待被 ready 的 Goroutine:
// checkTimeouts 恢復(fù)那些在等待一個(gè) note 且已經(jīng)觸發(fā)其 deadline 時(shí)的 Goroutine。 func checkTimeouts() { now := nanotime() for n, nt := range notesWithTimeout { if n.key == note_cleared && now > nt.deadline { n.key = note_timeout goready(nt.gp, 1) } } } func goready(gp *g, traceskip int) { systemstack(func() { ready(gp, traceskip, true) }) } // 將 gp 標(biāo)記為 ready 來運(yùn)行 func ready(gp *g, traceskip int, next bool) { if trace.enabled { traceGoUnpark(gp, traceskip) } status := readgstatus(gp) // 標(biāo)記為 runnable. _g_ := getg() _g_.m.locks++ // 禁止搶占,因?yàn)樗梢栽诰植孔兞恐斜4?nbsp;p if status&^_Gscan != _Gwaiting { dumpgstatus(gp) throw("bad g->status in ready") } // 狀態(tài)為 Gwaiting 或 Gscanwaiting, 標(biāo)記 Grunnable 并將其放入運(yùn)行隊(duì)列 runq casgstatus(gp, _Gwaiting, _Grunnable) runqput(_g_.m.p.ptr(), gp, next) if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 { wakep() } _g_.m.locks-- if _g_.m.locks == 0 && _g_.preempt { // 在 newstack 中已經(jīng)清除它的情況下恢復(fù)搶占請(qǐng)求 _g_.stackguard0 = stackPreempt } } func notetsleepg(n *note, ns int64) bool { gp := getg() ... if ns >= 0 { deadline := nanotime() + ns ... notesWithTimeout[n] = noteWithTimeout{gp: gp, deadline: deadline} ... gopark(nil, nil, waitReasonSleep, traceEvNone, 1) ... delete(notesWithTimeout, n) ... } ... }
而后通過 mcall 調(diào)用 gosched_m 在 g0 上繼續(xù)執(zhí)行并讓出 P,實(shí)質(zhì)上是讓 G 放棄當(dāng)前在 M 上的執(zhí)行權(quán)利,M 轉(zhuǎn)去執(zhí)行其他的 G,并在上下文切換時(shí)候,將自身放入全局隊(duì)列等待后續(xù)調(diào)度:
func goschedImpl(gp *g) { // 放棄當(dāng)前 g 的運(yùn)行狀態(tài) status := readgstatus(gp) ... casgstatus(gp, _Grunning, _Grunnable) // 使當(dāng)前 m 放棄 g dropg() // 并將 g 放回全局隊(duì)列中 lock(&sched.lock) globrunqput(gp) unlock(&sched.lock) // 重新進(jìn)入調(diào)度循環(huán) schedule() }
當(dāng)然,盡管具有主動(dòng)棄權(quán)的能力,但它對(duì) Go 語言的用戶要求比較高,因?yàn)橛脩粼诰帉懖l(fā)邏輯的時(shí)候需要自行甄別是否需要讓出時(shí)間片,這并非用戶友好的,而且很多 Go 的新用戶并不會(huì)了解到這個(gè)問題的存在,我們?cè)陔S后的搶占式調(diào)度中再進(jìn)一步展開討論。
主動(dòng)調(diào)度棄權(quán):棧擴(kuò)張與搶占標(biāo)記
另一種主動(dòng)放棄的方式是通過搶占標(biāo)記的方式實(shí)現(xiàn)的。基本想法是在每個(gè)函數(shù)調(diào)用的序言(函數(shù)調(diào)用的最前方)插入搶占檢測指令,當(dāng)檢測到當(dāng)前 Goroutine 被標(biāo)記為應(yīng)該被搶占時(shí),則主動(dòng)中斷執(zhí)行,讓出執(zhí)行權(quán)利。表面上看起來想法很簡單,但實(shí)施起來就比較復(fù)雜了。
在 6.6 執(zhí)行棧管理[2] 一節(jié)中我們已經(jīng)了解到,函數(shù)調(diào)用的序言部分會(huì)檢查 SP 寄存器與 stackguard0 之間的大小,如果 SP 小于 stackguard0 則會(huì)觸發(fā) morestack_noctxt,觸發(fā)棧分段操作。換言之,如果搶占標(biāo)記將 stackgard0 設(shè)為比所有可能的 SP 都要大(即 stackPreempt),則會(huì)觸發(fā) morestack,進(jìn)而調(diào)用 newstack:
// Goroutine 搶占請(qǐng)求 // 存儲(chǔ)到 g.stackguard0 來導(dǎo)致棧分段檢查失敗 // 必須比任何實(shí)際的 SP 都要大 // 十六進(jìn)制為:0xfffffade const stackPreempt = (1<<(8*sys.PtrSize) - 1) & -1314
從搶占調(diào)度的角度來看,這種發(fā)生在函數(shù)序言部分的搶占的一個(gè)重要目的就是能夠簡單且安全的記錄執(zhí)行現(xiàn)場(隨后的搶占式調(diào)度我們會(huì)看到記錄執(zhí)行現(xiàn)場給采用信號(hào)方式中斷線程執(zhí)行的調(diào)度帶來多大的困難)。事實(shí)也是如此,在 morestack 調(diào)用中:
TEXT runtime·morestack(SB),NOSPLIT,$0-0 ... MOVQ 0(SP), AX // f's PC MOVQ AX, (g_sched+gobuf_pc)(SI) MOVQ SI, (g_sched+gobuf_g)(SI) LEAQ 8(SP), AX // f's SP MOVQ AX, (g_sched+gobuf_sp)(SI) MOVQ BP, (g_sched+gobuf_bp)(SI) MOVQ DX, (g_sched+gobuf_ctxt)(SI) ... CALL runtime·newstack(SB)
是有記錄 Goroutine 的 PC 和 SP 寄存器,而后才開始調(diào)用 newstack 的:
//go:nowritebarrierrec func newstack() { thisg := getg() ... gp := thisg.m.curg ... morebuf := thisg.m.morebuf thisg.m.morebuf.pc = 0 thisg.m.morebuf.lr = 0 thisg.m.morebuf.sp = 0 thisg.m.morebuf.g = 0 // 如果是發(fā)起的搶占請(qǐng)求而非真正的棧分段 preempt := atomic.Loaduintptr(&gp.stackguard0) == stackPreempt // 保守的對(duì)用戶態(tài)代碼進(jìn)行搶占,而非搶占運(yùn)行時(shí)代碼 // 如果正持有鎖、分配內(nèi)存或搶占被禁用,則不發(fā)生搶占 if preempt { if !canPreemptM(thisg.m) { // 不發(fā)生搶占,繼續(xù)調(diào)度 gp.stackguard0 = gp.stack.lo + _StackGuard gogo(&gp.sched) // 重新進(jìn)入調(diào)度循環(huán) } } ... // 如果需要對(duì)棧進(jìn)行調(diào)整 if preempt { ... if gp.preemptShrink { // 我們正在一個(gè)同步安全點(diǎn),因此等待棧收縮 gp.preemptShrink = false shrinkstack(gp) } if gp.preemptStop { preemptPark(gp) // 永不返回 } ... // 表現(xiàn)得像是調(diào)用了 runtime.Gosched,主動(dòng)讓權(quán) gopreempt_m(gp) // 重新進(jìn)入調(diào)度循環(huán) } ... } // 與 gosched_m 一致 func gopreempt_m(gp *g) { ... goschedImpl(gp) }
其中的 canPreemptM 驗(yàn)證了可以被搶占的條件:
運(yùn)行時(shí)沒有禁止搶占(m.locks == 0)
運(yùn)行時(shí)沒有在執(zhí)行內(nèi)存分配(m.mallocing == 0)
運(yùn)行時(shí)沒有關(guān)閉搶占機(jī)制(m.preemptoff == "")
M 與 P 綁定且沒有進(jìn)入系統(tǒng)調(diào)用(p.status == _Prunning)
// canPreemptM 報(bào)告 mp 是否處于可搶占的安全狀態(tài)。 //go:nosplit func canPreemptM(mp *m) bool { return mp.locks == 0 && mp.mallocing == 0 && mp.preemptoff == "" && mp.p.ptr().status == _Prunning }
從可被搶占的條件來看,能夠?qū)σ粋€(gè) G 進(jìn)行搶占其實(shí)是呈保守狀態(tài)的。這一保守體現(xiàn)在搶占對(duì)很多運(yùn)行時(shí)所需的條件進(jìn)行了判斷,這也理所當(dāng)然是因?yàn)檫\(yùn)行時(shí)優(yōu)先級(jí)更高,不應(yīng)該輕易發(fā)生搶占,但與此同時(shí)由于又需要對(duì)用戶態(tài)代碼進(jìn)行搶占,于是先作出一次不需要搶占的判斷(快速路徑),確定不能搶占時(shí)返回并繼續(xù)調(diào)度,如果真的需要進(jìn)行搶占,則轉(zhuǎn)入調(diào)用 gopreempt_m,放棄當(dāng)前 G 的執(zhí)行權(quán),將其加入全局隊(duì)列,重新進(jìn)入調(diào)度循環(huán)。
什么時(shí)候會(huì)給 stackguard0 設(shè)置搶占標(biāo)記 stackPreempt 呢?一共有以下幾種情況:
進(jìn)入系統(tǒng)調(diào)用時(shí)(runtime.reentersyscall,注意這種情況是為了保證不會(huì)發(fā)生棧分裂,真正的搶占是異步地通過系統(tǒng)監(jiān)控進(jìn)行的)
任何運(yùn)行時(shí)不再持有鎖的時(shí)候(m.locks == 0)
當(dāng)垃圾回收器需要停止所有用戶 Goroutine 時(shí)
搶占式調(diào)度
從上面提到的兩種協(xié)作式調(diào)度邏輯我們可以看出,這種需要用戶代碼來主動(dòng)配合的調(diào)度方式存在一些致命的缺陷:一個(gè)沒有主動(dòng)放棄執(zhí)行權(quán)、且不參與任何函數(shù)調(diào)用的函數(shù),直到執(zhí)行完畢之前,是不會(huì)被搶占的。
那么這種不會(huì)被搶占的函數(shù)會(huì)導(dǎo)致什么嚴(yán)重的問題呢?回答是,由于運(yùn)行時(shí)無法停止該用戶代碼,則當(dāng)需要進(jìn)行垃圾回收時(shí),無法及時(shí)進(jìn)行;對(duì)于一些實(shí)時(shí)性要求較高的用戶態(tài) Goroutine 而言,也久久得不到調(diào)度。我們這里不去深入討論垃圾回收的具體細(xì)節(jié),讀者將在垃圾回收器[3]一章中詳細(xì)看到這類問題導(dǎo)致的后果。單從調(diào)度的角度而言,我們直接來看一個(gè)非常簡單的例子:
// 此程序在 Go 1.14 之前的版本不會(huì)輸出 OK package main import ( "runtime" "time" ) func main() { runtime.GOMAXPROCS(1) go func() { for { } }() time.Sleep(time.Millisecond) println("OK") }
這段代碼中處于死循環(huán)的 Goroutine 永遠(yuǎn)無法被搶占,其中創(chuàng)建的 Goroutine 會(huì)執(zhí)行一個(gè)不產(chǎn)生任何調(diào)用、不主動(dòng)放棄執(zhí)行權(quán)的死循環(huán)。由于主 Goroutine 優(yōu)先調(diào)用了休眠,此時(shí)唯一的 P 會(huì)轉(zhuǎn)去執(zhí)行 for 循環(huán)所創(chuàng)建的 Goroutine。進(jìn)而主 Goroutine 永遠(yuǎn)不會(huì)再被調(diào)度,進(jìn)而程序徹底阻塞在了這個(gè) Goroutine 上,永遠(yuǎn)無法退出。這樣的例子非常多,但追根溯源,均為此問題導(dǎo)致。
Go 團(tuán)隊(duì)其實(shí)很早(1.0 以前)就已經(jīng)意識(shí)到了這個(gè)問題,但在 Go 1.2 時(shí)增加了上文提到的在函數(shù)序言部分增加搶占標(biāo)記后,此問題便被擱置,直到越來越多的用戶提交并報(bào)告此問題。在 Go 1.5 前后,Austin Clements 希望僅解決這種由密集循環(huán)導(dǎo)致的無法搶占的問題 [Clements, 2015],于是嘗試通過協(xié)作式 loop 循環(huán)搶占,通過編譯器輔助的方式,插入搶占檢查指令,與流程圖回邊(指節(jié)點(diǎn)被訪問過但其子節(jié)點(diǎn)尚未訪問完畢)安全點(diǎn)(在一個(gè)線程執(zhí)行中,垃圾回收器能夠識(shí)別所有對(duì)象引用狀態(tài)的一個(gè)狀態(tài))的方式進(jìn)行解決。
盡管此舉能為搶占帶來顯著的提升,但是在一個(gè)循環(huán)中引入分支顯然會(huì)降低性能。盡管隨后 David Chase 對(duì)這個(gè)方法進(jìn)行了改進(jìn),僅在插入了一條 TESTB 指令 [Chase, 2017],在完全沒有分支以及寄存器壓力的情況下,仍然造成了幾何平均 7.8% 的性能損失。這種結(jié)果其實(shí)是情理之中的,很多需要進(jìn)行密集循環(huán)的計(jì)算時(shí)間都是在運(yùn)行時(shí)才能確定的,直接由編譯器檢測這類密集循環(huán)而插入額外的指令可想而知是欠妥的做法。
終于在 Go 1.10 后 [Clements, 2019],Austin 進(jìn)一步提出的解決方案,希望使用每個(gè)指令與執(zhí)行棧和寄存器的映射關(guān)系,通過記錄足夠多的信息,并通過異步線程來發(fā)送搶占信號(hào)的方式來支持異步搶占式調(diào)度。
我們知道現(xiàn)代操作系統(tǒng)的調(diào)度器多為搶占式調(diào)度,其實(shí)現(xiàn)方式通過硬件中斷來支持線程的切換,進(jìn)而能安全的保存運(yùn)行上下文。在 Go 運(yùn)行時(shí)實(shí)現(xiàn)搶占式調(diào)度同樣也可以使用類似的方式,通過向線程發(fā)送系統(tǒng)信號(hào)的方式來中斷 M 的執(zhí)行,進(jìn)而達(dá)到搶占的目的。但與操作系統(tǒng)的不同之處在于,由于運(yùn)行時(shí)諸多機(jī)制的存在(例如垃圾回收器),還必須能夠在 Goroutine 被停止時(shí),保存充足的上下文信息(見 8.9 安全點(diǎn)分析[4])。這就給中斷信號(hào)帶來了麻煩,如果中斷信號(hào)恰好發(fā)生在一些關(guān)鍵階段(例如寫屏障期間),則無法保證程序的正確性。這也就要求我們需要嚴(yán)格考慮觸發(fā)異步搶占的時(shí)機(jī)。
異步搶占式調(diào)度的一種方式就與運(yùn)行時(shí)系統(tǒng)監(jiān)控有關(guān),監(jiān)控循環(huán)會(huì)將發(fā)生阻塞的 Goroutine 搶占,解綁 P 與 M,從而讓其他的線程能夠獲得 P 繼續(xù)執(zhí)行其他的 Goroutine。這得益于 sysmon中調(diào)用的 retake 方法。這個(gè)方法處理了兩種搶占情況,一是搶占阻塞在系統(tǒng)調(diào)用上的 P,二是搶占運(yùn)行時(shí)間過長的 G。其中搶占運(yùn)行時(shí)間過長的 G 這一方式還會(huì)出現(xiàn)在垃圾回收需要進(jìn)入 STW 時(shí)。
P 搶占
我們先來看搶占阻塞在系統(tǒng)調(diào)用上的 G 這種情況。這種搶占的實(shí)現(xiàn)方法非常的自然,因?yàn)?Goroutine 已經(jīng)阻塞在了系統(tǒng)調(diào)用上,我們可以非常安全的將 M 與 P 進(jìn)行解綁,即便是 Goroutine 從阻塞中恢復(fù),也會(huì)檢查自身所在的 M 是否仍然持有 P,如果沒有 P 則重新考慮與可用的 P 進(jìn)行綁定。這種異步搶占的本質(zhì)是:搶占 P。
unc retake(now int64) uint32 { n := 0 // 防止 allp 數(shù)組發(fā)生變化,除非我們已經(jīng) STW,此鎖將完全沒有人競爭 lock(&allpLock) for i := 0; i < len(allp); i++ { _p_ := allp[i] ... pd := &_p_.sysmontick s := _p_.status sysretake := false if s == _Prunning || s == _Psyscall { // 如果 G 運(yùn)行時(shí)時(shí)間太長則進(jìn)行搶占 t := int64(_p_.schedtick) if int64(pd.schedtick) != t { pd.schedtick = uint32(t) pd.schedwhen = now } else if pd.schedwhen+forcePreemptNS <= now { ... sysretake = true } } // 對(duì)阻塞在系統(tǒng)調(diào)用上的 P 進(jìn)行搶占 if s == _Psyscall { // 如果已經(jīng)超過了一個(gè)系統(tǒng)監(jiān)控的 tick(20us),則從系統(tǒng)調(diào)用中搶占 P t := int64(_p_.syscalltick) if !sysretake && int64(pd.syscalltick) != t { pd.syscalltick = uint32(t) pd.syscallwhen = now continue } // 一方面,在沒有其他 work 的情況下,我們不希望搶奪 P // 另一方面,因?yàn)樗赡茏柚?nbsp;sysmon 線程從深度睡眠中喚醒,所以最終我們?nèi)韵M麚寠Z P if runqempty(_p_) && atomic.Load(&sched.nmspinning)+atomic.Load(&sched.npidle) > 0 && pd.syscallwhen+10*1000*1000 > now { continue } // 解除 allpLock,從而可以獲取 sched.lock unlock(&allpLock) // 在 CAS 之前需要減少空閑 M 的數(shù)量(假裝某個(gè)還在運(yùn)行) // 否則發(fā)生搶奪的 M 可能退出 syscall 然后再增加 nmidle ,進(jìn)而發(fā)生死鎖 // 這個(gè)過程發(fā)生在 stoplockedm 中 incidlelocked(-1) if atomic.Cas(&_p_.status, s, _Pidle) { // 將 P 設(shè)為 idle,從而交與其他 M 使用 ... n++ _p_.syscalltick++ handoffp(_p_) } incidlelocked(1) lock(&allpLock) } } unlock(&allpLock) return uint32(n) }
在搶占 P 的過程中,有兩個(gè)非常小心的處理方式:
鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)
如果此時(shí)隊(duì)列為空,那么完全沒有必要進(jìn)行搶占,這時(shí)候似乎可以繼續(xù)遍歷其他的 P,但必須在調(diào)度器中自旋的 M 和 空閑的 P 同時(shí)存在時(shí)、且系統(tǒng)調(diào)用阻塞時(shí)間非常長的情況下才能這么做。否則,這個(gè) retake 過程可能返回 0,進(jìn)而系統(tǒng)監(jiān)控可能看起來像是什么事情也沒做的情況下調(diào)整自己的步調(diào)進(jìn)入深度睡眠。
在將 P 設(shè)置為空閑狀態(tài)前,必須先將 M 的數(shù)量減少,否則當(dāng) M 退出系統(tǒng)調(diào)用時(shí),會(huì)在 exitsyscall0 中調(diào)用 stoplockedm 從而增加空閑 M 的數(shù)量,進(jìn)而發(fā)生死鎖。
M 搶占
在上面我們沒有展現(xiàn)一個(gè)細(xì)節(jié),那就是在檢查 P 的狀態(tài)時(shí),P 如果是運(yùn)行狀態(tài)會(huì)調(diào)用preemptone,來通過系統(tǒng)信號(hào)來完成搶占,之所以沒有在之前提及的原因在于該調(diào)用在 M 不與 P 綁定的情況下是不起任何作用直接返回的。這種異步搶占的本質(zhì)是:搶占 M。我們不妨繼續(xù)從系統(tǒng)監(jiān)控產(chǎn)生的搶占談起:
func retake(now int64) uint32 { ... for i := 0; i < len(allp); i++ { _p_ := allp[i] ... if s == _Prunning || s == _Psyscall { ... } else if pd.schedwhen+forcePreemptNS <= now { // 對(duì)于 syscall 的情況,因?yàn)?nbsp;M 沒有與 P 綁定, // preemptone() 不工作 preemptone(_p_) sysretake = true } } ... } ... } func preemptone(_p_ *p) bool { // 檢查 M 與 P 是否綁定 mp := _p_.m.ptr() if mp == nil || mp == getg().m { return false } gp := mp.curg if gp == nil || gp == mp.g0 { return false } // 將 G 標(biāo)記為搶占 gp.preempt = true // 一個(gè) Goroutine 中的每個(gè)調(diào)用都會(huì)通過比較當(dāng)前棧指針和 gp.stackgard0 // 來檢查棧是否溢出。 // 設(shè)置 gp.stackgard0 為 StackPreempt 來將搶占轉(zhuǎn)換為正常的棧溢出檢查。 gp.stackguard0 = stackPreempt // 請(qǐng)求該 P 的異步搶占 if preemptMSupported && debug.asyncpreemptoff == 0 { _p_.preempt = true preemptM(mp) } return true }
搶占信號(hào)的選取
preemptM 完成了信號(hào)的發(fā)送,其實(shí)現(xiàn)也非常直接,直接向需要進(jìn)行搶占的 M 發(fā)送 SIGURG 信號(hào)即可。但是真正的重要的問題是,為什么是 SIGURG 信號(hào)而不是其他的信號(hào)?如何才能保證該信號(hào)不與用戶態(tài)產(chǎn)生的信號(hào)產(chǎn)生沖突?這里面有幾個(gè)原因:
鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)
默認(rèn)情況下,SIGURG 已經(jīng)用于調(diào)試器傳遞信號(hào)。
SIGURG 可以不加選擇地虛假發(fā)生的信號(hào)。例如,我們不能選擇 SIGALRM,因?yàn)樾盘?hào)處理程序無法分辨它是否是由實(shí)際過程引起的(可以說這意味著信號(hào)已損壞)。而常見的用戶自定義信號(hào) SIGUSR1 和 SIGUSR2 也不夠好,因?yàn)橛脩魬B(tài)代碼可能會(huì)將其進(jìn)行使用。
需要處理沒有實(shí)時(shí)信號(hào)的平臺(tái)(例如 macOS)。
考慮以上的觀點(diǎn),SIGURG 其實(shí)是一個(gè)很好的、滿足所有這些條件、且極不可能因被用戶態(tài)代碼進(jìn)行使用的一種信號(hào)。
const sigPreempt = _SIGURG // preemptM 向 mp 發(fā)送搶占請(qǐng)求。該請(qǐng)求可以異步處理,也可以與對(duì) M 的其他請(qǐng)求合并。 // 接收到該請(qǐng)求后,如果正在運(yùn)行的 G 或 P 被標(biāo)記為搶占,并且 Goroutine 處于異步安全點(diǎn), // 它將搶占 Goroutine。在處理搶占請(qǐng)求后,它始終以原子方式遞增 mp.preemptGen。 func preemptM(mp *m) { ... signalM(mp, sigPreempt) } func signalM(mp *m, sig int) { tgkill(getpid(), int(mp.procid), sig) }
搶占調(diào)用的注入
我們?cè)谛盘?hào)處理一節(jié)[5]中已經(jīng)知道,每個(gè)運(yùn)行的 M 都會(huì)設(shè)置一個(gè)系統(tǒng)信號(hào)的處理的回調(diào),當(dāng)出現(xiàn)系統(tǒng)信號(hào)時(shí),操作系統(tǒng)將負(fù)責(zé)將運(yùn)行代碼進(jìn)行中斷,并安全的保護(hù)其執(zhí)行現(xiàn)場,進(jìn)而 Go 運(yùn)行時(shí)能將針對(duì)信號(hào)的類型進(jìn)行處理,當(dāng)信號(hào)處理函數(shù)執(zhí)行結(jié)束后,程序會(huì)再次進(jìn)入內(nèi)核空間,進(jìn)而恢復(fù)到被中斷的位置。
但是這里面有一個(gè)很巧妙的用法,因?yàn)?sighandler 能夠獲得操作系統(tǒng)所提供的執(zhí)行上下文參數(shù)(例如寄存器 rip, rep 等),如果在 sighandler 中修改了這個(gè)上下文參數(shù),OS 會(huì)根據(jù)就該的寄存器進(jìn)行恢復(fù),這也就為搶占提供了機(jī)會(huì)。
//go:nowritebarrierrec func sighandler(sig uint32, info *siginfo, ctxt unsafe.Pointer, gp *g) { ... c := &sigctxt{info, ctxt} ... if sig == sigPreempt { // 可能是一個(gè)搶占信號(hào) doSigPreempt(gp, c) // 即便這是一個(gè)搶占信號(hào),它也可能與其他信號(hào)進(jìn)行混合,因此我們 // 繼續(xù)進(jìn)行處理。 } ... } // doSigPreempt 處理了 gp 上的搶占信號(hào) func doSigPreempt(gp *g, ctxt *sigctxt) { // 檢查 G 是否需要被搶占、搶占是否安全 if wantAsyncPreempt(gp) && isAsyncSafePoint(gp, ctxt.sigpc(), ctxt.sigsp(), ctxt.siglr()) { // 插入搶占調(diào)用 ctxt.pushCall(funcPC(asyncPreempt)) } // 記錄搶占 atomic.Xadd(&gp.m.preemptGen, 1)
在 ctxt.pushCall 之前,ctxt.rip() 和 ctxt.rep() 都保存了被中斷的 Goroutine 所在的位置,但是 pushCall 直接修改了這些寄存器,進(jìn)而當(dāng)從 sighandler 返回用戶態(tài) Goroutine 時(shí),能夠從注入的 asyncPreempt 開始執(zhí)行:
func (c *sigctxt) pushCall(targetPC uintptr) { pc := uintptr(c.rip()) sp := uintptr(c.rsp()) sp -= sys.PtrSize *(*uintptr)(unsafe.Pointer(sp)) = pc c.set_rsp(uint64(sp)) c.set_rip(uint64(targetPC)) }
完成 sighandler 之,我們成功恢復(fù)到 asyncPreempt 調(diào)用:
// asyncPreempt 保存了所有用戶寄存器,并調(diào)用 asyncPreempt2 // // 當(dāng)棧掃描遭遇 asyncPreempt 棧幀時(shí),將會(huì)保守的掃描調(diào)用方棧幀 func asyncPreempt()
該函數(shù)的主要目的是保存用戶態(tài)寄存器,并且在調(diào)用完畢前恢復(fù)所有的寄存器上下文就好像什么事情都沒有發(fā)生過一樣:
TEXT ·asyncPreempt(SB),NOSPLIT|NOFRAME,$0-0 ... MOVQ AX, 0(SP) ... MOVUPS X15, 352(SP) CALL ·asyncPreempt2(SB) MOVUPS 352(SP), X15 ... MOVQ 0(SP), AX ... RET
當(dāng)調(diào)用 asyncPreempt2 時(shí),會(huì)根據(jù) preemptPark 或者 gopreempt_m 重新切換回調(diào)度循環(huán),從而打斷密集循環(huán)的繼續(xù)執(zhí)行。
//go:nosplit func asyncPreempt2() { gp := getg() gp.asyncSafePoint = true if gp.preemptStop { mcall(preemptPark) } else { mcall(gopreempt_m) } // 異步搶占過程結(jié)束 gp.asyncSafePoint = false }
至此,異步搶占過程結(jié)束。我們總結(jié)一下?lián)屨颊{(diào)用的整體邏輯:
M1 發(fā)送中斷信號(hào)(signalM(mp, sigPreempt))
M2 收到信號(hào),操作系統(tǒng)中斷其執(zhí)行代碼,并切換到信號(hào)處理函數(shù)(sighandler(signum, info, ctxt, gp))
M2 修改執(zhí)行的上下文,并恢復(fù)到修改后的位置(asyncPreempt)
重新進(jìn)入調(diào)度循環(huán)進(jìn)而調(diào)度其他 Goroutine(preemptPark 和 gopreempt_m)
上述的異步搶占流程我們是通過系統(tǒng)監(jiān)控來說明的,正如前面所提及的,異步搶占的本質(zhì)是在為垃圾回收器服務(wù),由于我們還沒有討論過 Go 語言垃圾回收的具體細(xì)節(jié),這里便不做過多展開,讀者只需理解,在垃圾回收周期開始時(shí),垃圾回收器將通過上述異步搶占的邏輯,停止所有用戶 Goroutine,進(jìn)而轉(zhuǎn)去執(zhí)行垃圾回收。
到此,關(guān)于“分析Go協(xié)作與搶占”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)砀鄬?shí)用的文章!
網(wǎng)站題目:分析Go協(xié)作與搶占
URL分享:http://m.newbst.com/article20/pohgjo.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站策劃、ChatGPT、網(wǎng)站導(dǎo)航、虛擬主機(jī)、移動(dòng)網(wǎng)站建設(shè)、外貿(mào)網(wǎng)站建設(shè)
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場,如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)