成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

聊一聊Go 協(xié)作與搶占

開發(fā) 前端
Go 的運(yùn)行時(shí)并不具備操作系統(tǒng)內(nèi)核級(jí)的硬件中斷能力,基于工作竊取的調(diào)度器實(shí)現(xiàn),本質(zhì)上屬于先來先服務(wù)的協(xié)作式調(diào)度,為了解決響應(yīng)時(shí)間可能較高的問題,目前運(yùn)行時(shí)實(shí)現(xiàn)了兩種不同的調(diào)度策略、每種策略各兩個(gè)形式。

[[323539]]

本文轉(zhuǎn)載自微信公眾號(hào)「 碼農(nóng)桃花源」,轉(zhuǎn)載本文請(qǐng)聯(lián)系 碼農(nóng)桃花源公眾號(hào)。

  • 協(xié)作式調(diào)度
    • 主動(dòng)用戶讓權(quán):Gosched
    • 主動(dòng)調(diào)度棄權(quán):棧擴(kuò)張與搶占標(biāo)記
  • 搶占式調(diào)度
    • P 搶占
    • M 搶占
  • 小結(jié)
  • 進(jìn)一步閱讀的參考文獻(xiàn)

我們?cè)诜治稣{(diào)度循環(huán)[1]的時(shí)候總結(jié)過一個(gè)問題:如果某個(gè) G 執(zhí)行時(shí)間過長,其他的 G 如何才能被正常地調(diào)度?這便涉及到有關(guān)調(diào)度的兩個(gè)理念:協(xié)作式調(diào)度與搶占式調(diào)度。

協(xié)作式和搶占式這兩個(gè)理念解釋起來很簡單:協(xié)作式調(diào)度依靠被調(diào)度方主動(dòng)棄權(quán);搶占式調(diào)度則依靠調(diào)度器強(qiáng)制將被調(diào)度方被動(dòng)中斷。這兩個(gè)概念其實(shí)描述了調(diào)度的兩種截然不同的策略,這兩種決策模式,在調(diào)度理論中其實(shí)已經(jīng)研究得很透徹了。

Go 的運(yùn)行時(shí)并不具備操作系統(tǒng)內(nèi)核級(jí)的硬件中斷能力,基于工作竊取的調(diào)度器實(shí)現(xiàn),本質(zhì)上屬于先來先服務(wù)的協(xié)作式調(diào)度,為了解決響應(yīng)時(shí)間可能較高的問題,目前運(yùn)行時(shí)實(shí)現(xiàn)了兩種不同的調(diào)度策略、每種策略各兩個(gè)形式。保證在大部分情況下,不同的 G 能夠獲得均勻的時(shí)間片:

  • 同步協(xié)作式調(diào)度
  1. 主動(dòng)用戶讓權(quán):通過 runtime.Gosched 調(diào)用主動(dòng)讓出執(zhí)行機(jī)會(huì);
  2. 主動(dòng)調(diào)度棄權(quán):當(dāng)發(fā)生執(zhí)行棧分段時(shí),檢查自身的搶占標(biāo)記,決定是否繼續(xù)執(zhí)行;
  • 異步搶占式調(diào)度
  1. 被動(dòng)監(jiān)控?fù)屨迹寒?dāng) G 阻塞在 M 上時(shí)(系統(tǒng)調(diào)用、channel 等),系統(tǒng)監(jiān)控會(huì)將 P 從 M 上搶奪并分配給其他的 M 來執(zhí)行其他的 G,而位于被搶奪 P 的本地調(diào)度隊(duì)列中的 G 則可能會(huì)被偷取到其他 M 執(zhí)行。
  2. 被動(dòng) GC 搶占:當(dāng)需要進(jìn)行垃圾回收時(shí),為了保證不具備主動(dòng)搶占處理的函數(shù)執(zhí)行時(shí)間過長,導(dǎo)致垃圾回收遲遲不能執(zhí)行而導(dǎo)致的高延遲,而強(qiáng)制停止 G 并轉(zhuǎn)為執(zhí)行垃圾回收。

協(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)非常簡單:

  1. // Gosched 會(huì)讓出當(dāng)前的 P,并允許其他 Goroutine 運(yùn)行。 
  2. // 它不會(huì)推遲當(dāng)前的 Goroutine,因此執(zhí)行會(huì)被自動(dòng)恢復(fù) 
  3. func Gosched() { 
  4.   checkTimeouts() 
  5.   mcall(gosched_m) 
  6.  
  7. // Gosched 在 g0 上繼續(xù)執(zhí)行 
  8. func gosched_m(gp *g) { 
  9.   ... 
  10.   goschedImpl(gp) 

它首先會(huì)通過 note 機(jī)制通知那些等待被 ready 的 Goroutine:

  1. // checkTimeouts 恢復(fù)那些在等待一個(gè) note 且已經(jīng)觸發(fā)其 deadline 時(shí)的 Goroutine。 
  2. func checkTimeouts() { 
  3.   now := nanotime() 
  4.   for n, nt := range notesWithTimeout { 
  5.     if n.key == note_cleared && now > nt.deadline { 
  6.       n.key = note_timeout 
  7.       goready(nt.gp, 1) 
  8.     } 
  9.   } 
  10.  
  11. func goready(gp *g, traceskip int) { 
  12.   systemstack(func() { 
  13.     ready(gp, traceskip, true
  14.   }) 
  15.  
  16. // 將 gp 標(biāo)記為 ready 來運(yùn)行 
  17. func ready(gp *g, traceskip intnext bool) { 
  18.   if trace.enabled { 
  19.     traceGoUnpark(gp, traceskip) 
  20.   } 
  21.  
  22.   status := readgstatus(gp) 
  23.  
  24.   // 標(biāo)記為 runnable. 
  25.   _g_ := getg() 
  26.   _g_.m.locks++ // 禁止搶占,因?yàn)樗梢栽诰植孔兞恐斜4?nbsp;p 
  27.   if status&^_Gscan != _Gwaiting { 
  28.     dumpgstatus(gp) 
  29.     throw("bad g->status in ready"
  30.   } 
  31.  
  32.   // 狀態(tài)為 Gwaiting 或 Gscanwaiting, 標(biāo)記 Grunnable 并將其放入運(yùn)行隊(duì)列 runq 
  33.   casgstatus(gp, _Gwaiting, _Grunnable) 
  34.   runqput(_g_.m.p.ptr(), gp, next
  35.   if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 { 
  36.     wakep() 
  37.   } 
  38.   _g_.m.locks-- 
  39.   if _g_.m.locks == 0 && _g_.preempt { // 在 newstack 中已經(jīng)清除它的情況下恢復(fù)搶占請(qǐng)求 
  40.     _g_.stackguard0 = stackPreempt 
  41.   } 
  42.  
  43. func notetsleepg(n *note, ns int64) bool { 
  44.   gp := getg() 
  45.   ... 
  46.  
  47.   if ns >= 0 { 
  48.     deadline := nanotime() + ns 
  49.     ... 
  50.     notesWithTimeout[n] = noteWithTimeout{gp: gp, deadline: deadline} 
  51.     ... 
  52.     gopark(nil, nil, waitReasonSleep, traceEvNone, 1) 
  53.     ... 
  54.     delete(notesWithTimeout, n) 
  55.     ... 
  56.   } 
  57.  
  58.   ... 

而后通過 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)度:

  1. func goschedImpl(gp *g) { 
  2.   // 放棄當(dāng)前 g 的運(yùn)行狀態(tài) 
  3.   status := readgstatus(gp) 
  4.   ... 
  5.   casgstatus(gp, _Grunning, _Grunnable) 
  6.   // 使當(dāng)前 m 放棄 g 
  7.   dropg() 
  8.   // 并將 g 放回全局隊(duì)列中 
  9.   lock(&sched.lock) 
  10.   globrunqput(gp) 
  11.   unlock(&sched.lock) 
  12.  
  13.   // 重新進(jìn)入調(diào)度循環(huán) 
  14.   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)用的最前方)插入搶占檢測(cè)指令,當(dāng)檢測(cè)到當(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:

  1. // Goroutine 搶占請(qǐng)求 
  2. // 存儲(chǔ)到 g.stackguard0 來導(dǎo)致棧分段檢查失敗 
  3. // 必須比任何實(shí)際的 SP 都要大 
  4. // 十六進(jìn)制為:0xfffffade 
  5. const stackPreempt = (1<<(8*sys.PtrSize) - 1) & -1314 

從搶占調(diào)度的角度來看,這種發(fā)生在函數(shù)序言部分的搶占的一個(gè)重要目的就是能夠簡單且安全的記錄執(zhí)行現(xiàn)場(chǎng)(隨后的搶占式調(diào)度我們會(huì)看到記錄執(zhí)行現(xiàn)場(chǎng)給采用信號(hào)方式中斷線程執(zhí)行的調(diào)度帶來多大的困難)。事實(shí)也是如此,在 morestack 調(diào)用中:

  1. TEXT runtime·morestack(SB),NOSPLIT,$0-0 
  2.     ... 
  3.     MOVQ    0(SP), AX // f's PC 
  4.     MOVQ    AX, (g_sched+gobuf_pc)(SI) 
  5.     MOVQ    SI, (g_sched+gobuf_g)(SI) 
  6.     LEAQ    8(SP), AX // f's SP 
  7.     MOVQ    AX, (g_sched+gobuf_sp)(SI) 
  8.     MOVQ    BP, (g_sched+gobuf_bp)(SI) 
  9.     MOVQ    DX, (g_sched+gobuf_ctxt)(SI) 
  10.     ... 
  11.     CALL    runtime·newstack(SB) 

是有記錄 Goroutine 的 PC 和 SP 寄存器,而后才開始調(diào)用 newstack 的:

  1. //go:nowritebarrierrec 
  2. func newstack() { 
  3.   thisg := getg() 
  4.   ... 
  5.  
  6.   gp := thisg.m.curg 
  7.   ... 
  8.  
  9.   morebuf := thisg.m.morebuf 
  10.   thisg.m.morebuf.pc = 0 
  11.   thisg.m.morebuf.lr = 0 
  12.   thisg.m.morebuf.sp = 0 
  13.   thisg.m.morebuf.g = 0 
  14.  
  15.   // 如果是發(fā)起的搶占請(qǐng)求而非真正的棧分段 
  16.   preempt := atomic.Loaduintptr(&gp.stackguard0) == stackPreempt 
  17.  
  18.   // 保守的對(duì)用戶態(tài)代碼進(jìn)行搶占,而非搶占運(yùn)行時(shí)代碼 
  19.   // 如果正持有鎖、分配內(nèi)存或搶占被禁用,則不發(fā)生搶占 
  20.   if preempt { 
  21.     if !canPreemptM(thisg.m) { 
  22.       // 不發(fā)生搶占,繼續(xù)調(diào)度 
  23.       gp.stackguard0 = gp.stack.lo + _StackGuard 
  24.       gogo(&gp.sched) // 重新進(jìn)入調(diào)度循環(huán) 
  25.     } 
  26.   } 
  27.   ... 
  28.   // 如果需要對(duì)棧進(jìn)行調(diào)整 
  29.   if preempt { 
  30.     ... 
  31.     if gp.preemptShrink { 
  32.       // 我們正在一個(gè)同步安全點(diǎn),因此等待棧收縮 
  33.       gp.preemptShrink = false 
  34.       shrinkstack(gp) 
  35.     } 
  36.     if gp.preemptStop { 
  37.       preemptPark(gp) // 永不返回 
  38.     } 
  39.     ... 
  40.     // 表現(xiàn)得像是調(diào)用了 runtime.Gosched,主動(dòng)讓權(quán) 
  41.     gopreempt_m(gp) // 重新進(jìn)入調(diào)度循環(huán) 
  42.   } 
  43.   ... 
  44. // 與 gosched_m 一致 
  45. func gopreempt_m(gp *g) { 
  46.   ... 
  47.   goschedImpl(gp) 

其中的 canPreemptM 驗(yàn)證了可以被搶占的條件:

  1. 運(yùn)行時(shí)沒有禁止搶占(m.locks == 0)
  2. 運(yùn)行時(shí)沒有在執(zhí)行內(nèi)存分配(m.mallocing == 0)
  3. 運(yùn)行時(shí)沒有關(guān)閉搶占機(jī)制(m.preemptoff == "")
  4. M 與 P 綁定且沒有進(jìn)入系統(tǒng)調(diào)用(p.status == _Prunning)
  1. // canPreemptM 報(bào)告 mp 是否處于可搶占的安全狀態(tài)。 
  2. //go:nosplit 
  3. func canPreemptM(mp *m) bool { 
  4.   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 呢?一共有以下幾種情況:

  1. 進(jìn)入系統(tǒng)調(diào)用時(shí)(runtime.reentersyscall,注意這種情況是為了保證不會(huì)發(fā)生棧分裂,真正的搶占是異步地通過系統(tǒng)監(jiān)控進(jìn)行的)
  2. 任何運(yùn)行時(shí)不再持有鎖的時(shí)候(m.locks == 0)
  3. 當(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è)非常簡單的例子:

  1. // 此程序在 Go 1.14 之前的版本不會(huì)輸出 OK 
  2. package main 
  3. import ( 
  4.   "runtime" 
  5.   "time" 
  6. func main() { 
  7.   runtime.GOMAXPROCS(1) 
  8.   go func() { 
  9.     for { 
  10.     } 
  11.   }() 
  12.   time.Sleep(time.Millisecond) 
  13.   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í)才能確定的,直接由編譯器檢測(cè)這類密集循環(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。

  1. unc retake(now int64) uint32 { 
  2.   n := 0 
  3.   // 防止 allp 數(shù)組發(fā)生變化,除非我們已經(jīng) STW,此鎖將完全沒有人競(jìng)爭 
  4.   lock(&allpLock) 
  5.   for i := 0; i < len(allp); i++ { 
  6.     _p_ := allp[i] 
  7.     ... 
  8.     pd := &_p_.sysmontick 
  9.     s := _p_.status 
  10.     sysretake := false 
  11.     if s == _Prunning || s == _Psyscall { 
  12.       // 如果 G 運(yùn)行時(shí)時(shí)間太長則進(jìn)行搶占 
  13.       t := int64(_p_.schedtick) 
  14.       if int64(pd.schedtick) != t { 
  15.         pd.schedtick = uint32(t) 
  16.         pd.schedwhen = now 
  17.       } else if pd.schedwhen+forcePreemptNS <= now { 
  18.         ... 
  19.         sysretake = true 
  20.       } 
  21.     } 
  22.     // 對(duì)阻塞在系統(tǒng)調(diào)用上的 P 進(jìn)行搶占 
  23.     if s == _Psyscall { 
  24.       // 如果已經(jīng)超過了一個(gè)系統(tǒng)監(jiān)控的 tick(20us),則從系統(tǒng)調(diào)用中搶占 P 
  25.       t := int64(_p_.syscalltick) 
  26.       if !sysretake && int64(pd.syscalltick) != t { 
  27.         pd.syscalltick = uint32(t) 
  28.         pd.syscallwhen = now 
  29.         continue 
  30.       } 
  31.       // 一方面,在沒有其他 work 的情況下,我們不希望搶奪 P 
  32.       // 另一方面,因?yàn)樗赡茏柚?nbsp;sysmon 線程從深度睡眠中喚醒,所以最終我們?nèi)韵M麚寠Z P 
  33.       if runqempty(_p_) && atomic.Load(&sched.nmspinning)+atomic.Load(&sched.npidle) > 0 && pd.syscallwhen+10*1000*1000 > now { 
  34.         continue 
  35.       } 
  36.       // 解除 allpLock,從而可以獲取 sched.lock 
  37.       unlock(&allpLock) 
  38.       // 在 CAS 之前需要減少空閑 M 的數(shù)量(假裝某個(gè)還在運(yùn)行) 
  39.       // 否則發(fā)生搶奪的 M 可能退出 syscall 然后再增加 nmidle ,進(jìn)而發(fā)生死鎖 
  40.       // 這個(gè)過程發(fā)生在 stoplockedm 中 
  41.       incidlelocked(-1) 
  42.       if atomic.Cas(&_p_.status, s, _Pidle) { // 將 P 設(shè)為 idle,從而交與其他 M 使用 
  43.         ... 
  44.         n++ 
  45.         _p_.syscalltick++ 
  46.         handoffp(_p_) 
  47.       } 
  48.       incidlelocked(1) 
  49.       lock(&allpLock) 
  50.     } 
  51.   } 
  52.   unlock(&allpLock) 
  53.   return uint32(n) 

在搶占 P 的過程中,有兩個(gè)非常小心的處理方式:

  1. 如果此時(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)入深度睡眠。
  2. 在將 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)生的搶占談起:

  1. func retake(now int64) uint32 { 
  2.   ... 
  3.   for i := 0; i < len(allp); i++ { 
  4.     _p_ := allp[i] 
  5.     ... 
  6.     if s == _Prunning || s == _Psyscall { 
  7.       ... 
  8.       } else if pd.schedwhen+forcePreemptNS <= now { 
  9.         // 對(duì)于 syscall 的情況,因?yàn)?nbsp;M 沒有與 P 綁定, 
  10.         // preemptone() 不工作 
  11.         preemptone(_p_) 
  12.         sysretake = true 
  13.       } 
  14.     } 
  15.     ... 
  16.   } 
  17.   ... 
  18. func preemptone(_p_ *p) bool { 
  19.   // 檢查 M 與 P 是否綁定 
  20.   mp := _p_.m.ptr() 
  21.   if mp == nil || mp == getg().m { 
  22.     return false 
  23.   } 
  24.   gp := mp.curg 
  25.   if gp == nil || gp == mp.g0 { 
  26.     return false 
  27.   } 
  28.  
  29.   // 將 G 標(biāo)記為搶占 
  30.   gp.preempt = true 
  31.  
  32.   // 一個(gè) Goroutine 中的每個(gè)調(diào)用都會(huì)通過比較當(dāng)前棧指針和 gp.stackgard0 
  33.   // 來檢查棧是否溢出。 
  34.   // 設(shè)置 gp.stackgard0 為 StackPreempt 來將搶占轉(zhuǎn)換為正常的棧溢出檢查。 
  35.   gp.stackguard0 = stackPreempt 
  36.  
  37.   // 請(qǐng)求該 P 的異步搶占 
  38.   if preemptMSupported && debug.asyncpreemptoff == 0 { 
  39.     _p_.preempt = true 
  40.     preemptM(mp) 
  41.   } 
  42.  
  43.   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è)原因:

  1. 默認(rèn)情況下,SIGURG 已經(jīng)用于調(diào)試器傳遞信號(hào)。
  2. SIGURG 可以不加選擇地虛假發(fā)生的信號(hào)。例如,我們不能選擇 SIGALRM,因?yàn)樾盘?hào)處理程序無法分辨它是否是由實(shí)際過程引起的(可以說這意味著信號(hào)已損壞)。而常見的用戶自定義信號(hào) SIGUSR1 和 SIGUSR2 也不夠好,因?yàn)橛脩魬B(tài)代碼可能會(huì)將其進(jìn)行使用。
  3. 需要處理沒有實(shí)時(shí)信號(hào)的平臺(tái)(例如 macOS)。

考慮以上的觀點(diǎn),SIGURG 其實(shí)是一個(gè)很好的、滿足所有這些條件、且極不可能因被用戶態(tài)代碼進(jìn)行使用的一種信號(hào)。

  1. const sigPreempt = _SIGURG 
  2.  
  3. // preemptM 向 mp 發(fā)送搶占請(qǐng)求。該請(qǐng)求可以異步處理,也可以與對(duì) M 的其他請(qǐng)求合并。 
  4. // 接收到該請(qǐng)求后,如果正在運(yùn)行的 G 或 P 被標(biāo)記為搶占,并且 Goroutine 處于異步安全點(diǎn), 
  5. // 它將搶占 Goroutine。在處理搶占請(qǐng)求后,它始終以原子方式遞增 mp.preemptGen。 
  6. func preemptM(mp *m) { 
  7.   ... 
  8.   signalM(mp, sigPreempt) 
  9. func signalM(mp *m, sig int) { 
  10.   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)場(chǎng),進(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ì)。

  1. //go:nowritebarrierrec 
  2. func sighandler(sig uint32, info *siginfo, ctxt unsafe.Pointer, gp *g) { 
  3.   ... 
  4.   c := &sigctxt{info, ctxt} 
  5.   ... 
  6.   if sig == sigPreempt { 
  7.     // 可能是一個(gè)搶占信號(hào) 
  8.     doSigPreempt(gp, c) 
  9.     // 即便這是一個(gè)搶占信號(hào),它也可能與其他信號(hào)進(jìn)行混合,因此我們 
  10.     // 繼續(xù)進(jìn)行處理。 
  11.   } 
  12.   ... 
  13. // doSigPreempt 處理了 gp 上的搶占信號(hào) 
  14. func doSigPreempt(gp *g, ctxt *sigctxt) { 
  15.   // 檢查 G 是否需要被搶占、搶占是否安全 
  16.   if wantAsyncPreempt(gp) && isAsyncSafePoint(gp, ctxt.sigpc(), ctxt.sigsp(), ctxt.siglr()) { 
  17.     // 插入搶占調(diào)用 
  18.     ctxt.pushCall(funcPC(asyncPreempt)) 
  19.   } 
  20.  
  21.   // 記錄搶占 
  22.   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í)行:

  1. func (c *sigctxt) pushCall(targetPC uintptr) { 
  2.   pc := uintptr(c.rip()) 
  3.   sp := uintptr(c.rsp()) 
  4.   sp -= sys.PtrSize 
  5.   *(*uintptr)(unsafe.Pointer(sp)) = pc 
  6.   c.set_rsp(uint64(sp)) 
  7.   c.set_rip(uint64(targetPC)) 

完成 sighandler 之,我們成功恢復(fù)到 asyncPreempt 調(diào)用:

  1. // asyncPreempt 保存了所有用戶寄存器,并調(diào)用 asyncPreempt2 
  2. // 
  3. // 當(dāng)棧掃描遭遇 asyncPreempt 棧幀時(shí),將會(huì)保守的掃描調(diào)用方棧幀 
  4. func asyncPreempt() 

該函數(shù)的主要目的是保存用戶態(tài)寄存器,并且在調(diào)用完畢前恢復(fù)所有的寄存器上下文就好像什么事情都沒有發(fā)生過一樣:

  1. TEXT ·asyncPreempt(SB),NOSPLIT|NOFRAME,$0-0 
  2.     ... 
  3.     MOVQ AX, 0(SP) 
  4.     ... 
  5.     MOVUPS X15, 352(SP) 
  6.     CALL ·asyncPreempt2(SB) 
  7.     MOVUPS 352(SP), X15 
  8.     ... 
  9.     MOVQ 0(SP), AX 
  10.     ... 
  11.     RET 

當(dāng)調(diào)用 asyncPreempt2 時(shí),會(huì)根據(jù) preemptPark 或者 gopreempt_m 重新切換回調(diào)度循環(huán),從而打斷密集循環(huán)的繼續(xù)執(zhí)行。

  1. //go:nosplit 
  2. func asyncPreempt2() { 
  3.   gp := getg() 
  4.   gp.asyncSafePoint = true 
  5.   if gp.preemptStop { 
  6.     mcall(preemptPark) 
  7.   } else { 
  8.     mcall(gopreempt_m) 
  9.   } 
  10.   // 異步搶占過程結(jié)束 
  11.   gp.asyncSafePoint = false 

至此,異步搶占過程結(jié)束。我們總結(jié)一下?lián)屨颊{(diào)用的整體邏輯:

  1. M1 發(fā)送中斷信號(hào)(signalM(mp, sigPreempt))
  2. M2 收到信號(hào),操作系統(tǒng)中斷其執(zhí)行代碼,并切換到信號(hào)處理函數(shù)(sighandler(signum, info, ctxt, gp))
  3. M2 修改執(zhí)行的上下文,并恢復(fù)到修改后的位置(asyncPreempt)
  4. 重新進(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í)行垃圾回收。

小結(jié)

總的來說,應(yīng)用層的調(diào)度策略不易實(shí)現(xiàn),因此實(shí)現(xiàn)上也并不是特別緊急。我們回顧 Go 語言調(diào)度策略的調(diào)整過程不難發(fā)現(xiàn),實(shí)現(xiàn)它們的動(dòng)力是從實(shí)際需求出發(fā)的。Go 語言從設(shè)計(jì)之初并沒有刻意地去考慮對(duì) Goroutine 的搶占機(jī)制。從早期無法對(duì) Goroutine 進(jìn)行搶占的原始時(shí)代,到現(xiàn)在的協(xié)作與搶占同時(shí)配合的調(diào)度策略,其問題的核心是垃圾回收的需要。

運(yùn)行時(shí)需要執(zhí)行垃圾回收時(shí),協(xié)作式調(diào)度能夠保證具備函數(shù)調(diào)用的用戶 Goroutine 正常停止;搶占式調(diào)度則能避免由于死循環(huán)導(dǎo)致的任意時(shí)間的垃圾回收延遲。至此,Go 語言的用戶可以放心地寫出各種形式的代碼邏輯,運(yùn)行時(shí)垃圾回收也能夠在適當(dāng)?shù)臅r(shí)候及時(shí)中斷用戶代碼,不至于導(dǎo)致整個(gè)系統(tǒng)進(jìn)入不可預(yù)測(cè)的停頓。

進(jìn)一步閱讀的參考文獻(xiàn)[Clements, 2019] Austin Clements. Proposal: Non-cooperative goroutine preemption. January 18, 2019. https://github.com/golang/proposal/blob/master/design/24543-non-cooperative-preemption.md

[Clements, 2015] Austin Clements. runtime: tight loops should be preemptible](https://golang.org/issue/10958

[Chase, 2017] David Chase. cmd/compile: loop preemption with "fault branch" on amd64. May 09, 2019. https://golang.org/cl/43050

參考資料

[1]分析調(diào)度循環(huán): https://changkun.de/golang/zh-cn/part2runtime/ch06sched/exec[2]6.6 執(zhí)行棧管理: https://changkun.de/golang/zh-cn/part2runtime/ch06sched/stack/[3]垃圾回收器: https://github.com/qcrao/Go-Questions/blob/master/GC/GC.md[4]8.9 安全點(diǎn)分析: https://changkun.de/golang/zh-cn/part2runtime/ch08gc/safe[5]信號(hào)處理一節(jié): https://changkun.de/golang/zh-cn/part2runtime/ch06sched/signal/

本文作者歐長坤,德國慕尼黑大學(xué)在讀博士,Go/etcd/Tensorflow contributor,開源書籍《Go 語言原本》作者,《Go 夜讀》SIG 成員/講師,對(duì) Go 有很深的研究。Github:@changkun,https://changkun.de。

責(zé)任編輯:武曉燕 來源: 碼農(nóng)桃花源
相關(guān)推薦

2021-04-15 12:10:42

Go語言Go開發(fā)者

2022-03-31 10:41:35

iOS應(yīng)用提審發(fā)布

2018-03-23 10:30:56

微網(wǎng)關(guān)服務(wù)嚙合微服務(wù)

2021-08-11 09:37:11

Redis持久化磁盤

2023-09-27 09:04:50

2021-09-15 14:52:43

數(shù)字貨幣傳銷虛擬貨幣

2023-09-22 17:36:37

2021-01-28 22:31:33

分組密碼算法

2020-05-22 08:16:07

PONGPONXG-PON

2021-08-04 10:15:14

Go路徑語言

2018-06-07 13:17:12

契約測(cè)試單元測(cè)試API測(cè)試

2019-02-13 14:15:59

Linux版本Fedora

2021-08-04 09:32:05

Typescript 技巧Partial

2021-01-29 08:32:21

數(shù)據(jù)結(jié)構(gòu)數(shù)組

2021-02-06 08:34:49

函數(shù)memoize文檔

2022-08-08 08:25:21

Javajar 文件

2018-11-29 09:13:47

CPU中斷控制器

2022-11-01 08:46:20

責(zé)任鏈模式對(duì)象

2023-07-06 13:56:14

微軟Skype

2023-05-15 08:38:58

模板方法模式
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

主站蜘蛛池模板: 天天操夜夜骑 | av黄色免费在线观看 | 久久国产综合 | 一区二区在线看 | av网站观看| 精品麻豆剧传媒av国产九九九 | 亚洲国产免费 | 欧美日韩精品久久久免费观看 | 成人精品一区 | 国产欧美一区二区三区久久人妖 | 久久精品国产一区二区电影 | 免费久久久久久 | 亚洲精品中文在线观看 | 国产一区二区在线免费观看 | 日韩色在线 | 亚洲精精品 | 日本三级电影在线看 | 亚洲aⅴ | 国产激情亚洲 | 黄色亚洲| 狠狠色狠狠色综合日日92 | 欧美日韩在线观看视频 | 中文字幕精品一区 | 99re6热在线精品视频播放 | 91九色视频 | 国产精品毛片一区二区在线看 | 九热在线 | 91大神xh98xh系列全部 | 日韩免费av网站 | 久久狠狠 | a视频在线观看 | 午夜av成人 | 国产国拍亚洲精品av | 久久小视频 | 亚洲一区中文字幕在线观看 | 日韩高清成人 | 亚洲激情一区二区 | 国产精品久久久久久久久免费樱桃 | 久久久www成人免费精品张筱雨 | 久久久久国产精品免费免费搜索 | 涩爱av一区二区三区 |