精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

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

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

[[323539]]

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

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

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

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

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

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

協(xié)作式調(diào)度

主動用戶讓權(quán):Gosched

Gosched 是一種主動放棄執(zhí)行的手段,用戶態(tài)代碼通過調(diào)用此接口來出讓執(zhí)行機會,使其他“人”也能在密集的執(zhí)行過程中獲得被調(diào)度的機會。

Gosched 的實現(xiàn)非常簡單:

  1. // Gosched 會讓出當(dāng)前的 P,并允許其他 Goroutine 運行。 
  2. // 它不會推遲當(dāng)前的 Goroutine,因此執(zhí)行會被自動恢復(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) 

它首先會通過 note 機制通知那些等待被 ready 的 Goroutine:

  1. // checkTimeouts 恢復(fù)那些在等待一個 note 且已經(jīng)觸發(fā)其 deadline 時的 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 標記為 ready 來運行 
  17. func ready(gp *g, traceskip intnext bool) { 
  18.   if trace.enabled { 
  19.     traceGoUnpark(gp, traceskip) 
  20.   } 
  21.  
  22.   status := readgstatus(gp) 
  23.  
  24.   // 標記為 runnable. 
  25.   _g_ := getg() 
  26.   _g_.m.locks++ // 禁止搶占,因為它可以在局部變量中保存 p 
  27.   if status&^_Gscan != _Gwaiting { 
  28.     dumpgstatus(gp) 
  29.     throw("bad g->status in ready"
  30.   } 
  31.  
  32.   // 狀態(tài)為 Gwaiting 或 Gscanwaiting, 標記 Grunnable 并將其放入運行隊列 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ù)搶占請求 
  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,實質(zhì)上是讓 G 放棄當(dāng)前在 M 上的執(zhí)行權(quán)利,M 轉(zhuǎn)去執(zhí)行其他的 G,并在上下文切換時候,將自身放入全局隊列等待后續(xù)調(diào)度:

  1. func goschedImpl(gp *g) { 
  2.   // 放棄當(dāng)前 g 的運行狀態(tài) 
  3.   status := readgstatus(gp) 
  4.   ... 
  5.   casgstatus(gp, _Grunning, _Grunnable) 
  6.   // 使當(dāng)前 m 放棄 g 
  7.   dropg() 
  8.   // 并將 g 放回全局隊列中 
  9.   lock(&sched.lock) 
  10.   globrunqput(gp) 
  11.   unlock(&sched.lock) 
  12.  
  13.   // 重新進入調(diào)度循環(huán) 
  14.   schedule() 

當(dāng)然,盡管具有主動棄權(quán)的能力,但它對 Go 語言的用戶要求比較高,因為用戶在編寫并發(fā)邏輯的時候需要自行甄別是否需要讓出時間片,這并非用戶友好的,而且很多 Go 的新用戶并不會了解到這個問題的存在,我們在隨后的搶占式調(diào)度中再進一步展開討論。

主動調(diào)度棄權(quán):棧擴張與搶占標記

另一種主動放棄的方式是通過搶占標記的方式實現(xiàn)的。基本想法是在每個函數(shù)調(diào)用的序言(函數(shù)調(diào)用的最前方)插入搶占檢測指令,當(dāng)檢測到當(dāng)前 Goroutine 被標記為應(yīng)該被搶占時,則主動中斷執(zhí)行,讓出執(zhí)行權(quán)利。表面上看起來想法很簡單,但實施起來就比較復(fù)雜了。

在 6.6 執(zhí)行棧管理[2] 一節(jié)中我們已經(jīng)了解到,函數(shù)調(diào)用的序言部分會檢查 SP 寄存器與 stackguard0 之間的大小,如果 SP 小于 stackguard0 則會觸發(fā) morestack_noctxt,觸發(fā)棧分段操作。換言之,如果搶占標記將 stackgard0 設(shè)為比所有可能的 SP 都要大(即 stackPreempt),則會觸發(fā) morestack,進而調(diào)用 newstack:

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

從搶占調(diào)度的角度來看,這種發(fā)生在函數(shù)序言部分的搶占的一個重要目的就是能夠簡單且安全的記錄執(zhí)行現(xiàn)場(隨后的搶占式調(diào)度我們會看到記錄執(zhí)行現(xiàn)場給采用信號方式中斷線程執(zhí)行的調(diào)度帶來多大的困難)。事實也是如此,在 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ā)起的搶占請求而非真正的棧分段 
  16.   preempt := atomic.Loaduintptr(&gp.stackguard0) == stackPreempt 
  17.  
  18.   // 保守的對用戶態(tài)代碼進行搶占,而非搶占運行時代碼 
  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) // 重新進入調(diào)度循環(huán) 
  25.     } 
  26.   } 
  27.   ... 
  28.   // 如果需要對棧進行調(diào)整 
  29.   if preempt { 
  30.     ... 
  31.     if gp.preemptShrink { 
  32.       // 我們正在一個同步安全點,因此等待棧收縮 
  33.       gp.preemptShrink = false 
  34.       shrinkstack(gp) 
  35.     } 
  36.     if gp.preemptStop { 
  37.       preemptPark(gp) // 永不返回 
  38.     } 
  39.     ... 
  40.     // 表現(xiàn)得像是調(diào)用了 runtime.Gosched,主動讓權(quán) 
  41.     gopreempt_m(gp) // 重新進入調(diào)度循環(huán) 
  42.   } 
  43.   ... 
  44. // 與 gosched_m 一致 
  45. func gopreempt_m(gp *g) { 
  46.   ... 
  47.   goschedImpl(gp) 

其中的 canPreemptM 驗證了可以被搶占的條件:

  1. 運行時沒有禁止搶占(m.locks == 0)
  2. 運行時沒有在執(zhí)行內(nèi)存分配(m.mallocing == 0)
  3. 運行時沒有關(guān)閉搶占機制(m.preemptoff == "")
  4. M 與 P 綁定且沒有進入系統(tǒng)調(diào)用(p.status == _Prunning)
  1. // canPreemptM 報告 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 進行搶占其實是呈保守狀態(tài)的。這一保守體現(xiàn)在搶占對很多運行時所需的條件進行了判斷,這也理所當(dāng)然是因為運行時優(yōu)先級更高,不應(yīng)該輕易發(fā)生搶占,但與此同時由于又需要對用戶態(tài)代碼進行搶占,于是先作出一次不需要搶占的判斷(快速路徑),確定不能搶占時返回并繼續(xù)調(diào)度,如果真的需要進行搶占,則轉(zhuǎn)入調(diào)用 gopreempt_m,放棄當(dāng)前 G 的執(zhí)行權(quán),將其加入全局隊列,重新進入調(diào)度循環(huán)。

什么時候會給 stackguard0 設(shè)置搶占標記 stackPreempt 呢?一共有以下幾種情況:

  1. 進入系統(tǒng)調(diào)用時(runtime.reentersyscall,注意這種情況是為了保證不會發(fā)生棧分裂,真正的搶占是異步地通過系統(tǒng)監(jiān)控進行的)
  2. 任何運行時不再持有鎖的時候(m.locks == 0)
  3. 當(dāng)垃圾回收器需要停止所有用戶 Goroutine 時

搶占式調(diào)度

從上面提到的兩種協(xié)作式調(diào)度邏輯我們可以看出,這種需要用戶代碼來主動配合的調(diào)度方式存在一些致命的缺陷:一個沒有主動放棄執(zhí)行權(quán)、且不參與任何函數(shù)調(diào)用的函數(shù),直到執(zhí)行完畢之前,是不會被搶占的。

那么這種不會被搶占的函數(shù)會導(dǎo)致什么嚴重的問題呢?回答是,由于運行時無法停止該用戶代碼,則當(dāng)需要進行垃圾回收時,無法及時進行;對于一些實時性要求較高的用戶態(tài) Goroutine 而言,也久久得不到調(diào)度。我們這里不去深入討論垃圾回收的具體細節(jié),讀者將在垃圾回收器[3]一章中詳細看到這類問題導(dǎo)致的后果。單從調(diào)度的角度而言,我們直接來看一個非常簡單的例子:

  1. // 此程序在 Go 1.14 之前的版本不會輸出 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 永遠無法被搶占,其中創(chuàng)建的 Goroutine 會執(zhí)行一個不產(chǎn)生任何調(diào)用、不主動放棄執(zhí)行權(quán)的死循環(huán)。由于主 Goroutine 優(yōu)先調(diào)用了休眠,此時唯一的 P 會轉(zhuǎn)去執(zhí)行 for 循環(huán)所創(chuàng)建的 Goroutine。進而主 Goroutine 永遠不會再被調(diào)度,進而程序徹底阻塞在了這個 Goroutine 上,永遠無法退出。這樣的例子非常多,但追根溯源,均為此問題導(dǎo)致。

Go 團隊其實很早(1.0 以前)就已經(jīng)意識到了這個問題,但在 Go 1.2 時增加了上文提到的在函數(shù)序言部分增加搶占標記后,此問題便被擱置,直到越來越多的用戶提交并報告此問題。在 Go 1.5 前后,Austin Clements 希望僅解決這種由密集循環(huán)導(dǎo)致的無法搶占的問題 [Clements, 2015],于是嘗試通過協(xié)作式 loop 循環(huán)搶占,通過編譯器輔助的方式,插入搶占檢查指令,與流程圖回邊(指節(jié)點被訪問過但其子節(jié)點尚未訪問完畢)安全點(在一個線程執(zhí)行中,垃圾回收器能夠識別所有對象引用狀態(tài)的一個狀態(tài))的方式進行解決。

盡管此舉能為搶占帶來顯著的提升,但是在一個循環(huán)中引入分支顯然會降低性能。盡管隨后 David Chase 對這個方法進行了改進,僅在插入了一條 TESTB 指令 [Chase, 2017],在完全沒有分支以及寄存器壓力的情況下,仍然造成了幾何平均 7.8% 的性能損失。這種結(jié)果其實是情理之中的,很多需要進行密集循環(huán)的計算時間都是在運行時才能確定的,直接由編譯器檢測這類密集循環(huán)而插入額外的指令可想而知是欠妥的做法。

終于在 Go 1.10 后 [Clements, 2019],Austin 進一步提出的解決方案,希望使用每個指令與執(zhí)行棧和寄存器的映射關(guān)系,通過記錄足夠多的信息,并通過異步線程來發(fā)送搶占信號的方式來支持異步搶占式調(diào)度。

我們知道現(xiàn)代操作系統(tǒng)的調(diào)度器多為搶占式調(diào)度,其實現(xiàn)方式通過硬件中斷來支持線程的切換,進而能安全的保存運行上下文。在 Go 運行時實現(xiàn)搶占式調(diào)度同樣也可以使用類似的方式,通過向線程發(fā)送系統(tǒng)信號的方式來中斷 M 的執(zhí)行,進而達到搶占的目的。但與操作系統(tǒng)的不同之處在于,由于運行時諸多機制的存在(例如垃圾回收器),還必須能夠在 Goroutine 被停止時,保存充足的上下文信息(見 8.9 安全點分析[4])。這就給中斷信號帶來了麻煩,如果中斷信號恰好發(fā)生在一些關(guān)鍵階段(例如寫屏障期間),則無法保證程序的正確性。這也就要求我們需要嚴格考慮觸發(fā)異步搶占的時機。

異步搶占式調(diào)度的一種方式就與運行時系統(tǒng)監(jiān)控有關(guān),監(jiān)控循環(huán)會將發(fā)生阻塞的 Goroutine 搶占,解綁 P 與 M,從而讓其他的線程能夠獲得 P 繼續(xù)執(zhí)行其他的 Goroutine。這得益于 sysmon中調(diào)用的 retake 方法。這個方法處理了兩種搶占情況,一是搶占阻塞在系統(tǒng)調(diào)用上的 P,二是搶占運行時間過長的 G。其中搶占運行時間過長的 G 這一方式還會出現(xiàn)在垃圾回收需要進入 STW 時。

P 搶占

我們先來看搶占阻塞在系統(tǒng)調(diào)用上的 G 這種情況。這種搶占的實現(xiàn)方法非常的自然,因為 Goroutine 已經(jīng)阻塞在了系統(tǒng)調(diào)用上,我們可以非常安全的將 M 與 P 進行解綁,即便是 Goroutine 從阻塞中恢復(fù),也會檢查自身所在的 M 是否仍然持有 P,如果沒有 P 則重新考慮與可用的 P 進行綁定。這種異步搶占的本質(zhì)是:搶占 P。

  1. unc retake(now int64) uint32 { 
  2.   n := 0 
  3.   // 防止 allp 數(shù)組發(fā)生變化,除非我們已經(jīng) STW,此鎖將完全沒有人競爭 
  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 運行時時間太長則進行搶占 
  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.     // 對阻塞在系統(tǒng)調(diào)用上的 P 進行搶占 
  23.     if s == _Psyscall { 
  24.       // 如果已經(jīng)超過了一個系統(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.       // 另一方面,因為它可能阻止 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ù)量(假裝某個還在運行) 
  39.       // 否則發(fā)生搶奪的 M 可能退出 syscall 然后再增加 nmidle ,進而發(fā)生死鎖 
  40.       // 這個過程發(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 的過程中,有兩個非常小心的處理方式:

  1. 如果此時隊列為空,那么完全沒有必要進行搶占,這時候似乎可以繼續(xù)遍歷其他的 P,但必須在調(diào)度器中自旋的 M 和 空閑的 P 同時存在時、且系統(tǒng)調(diào)用阻塞時間非常長的情況下才能這么做。否則,這個 retake 過程可能返回 0,進而系統(tǒng)監(jiān)控可能看起來像是什么事情也沒做的情況下調(diào)整自己的步調(diào)進入深度睡眠。
  2. 在將 P 設(shè)置為空閑狀態(tài)前,必須先將 M 的數(shù)量減少,否則當(dāng) M 退出系統(tǒng)調(diào)用時,會在 exitsyscall0 中調(diào)用 stoplockedm 從而增加空閑 M 的數(shù)量,進而發(fā)生死鎖。

M 搶占

在上面我們沒有展現(xiàn)一個細節(jié),那就是在檢查 P 的狀態(tài)時,P 如果是運行狀態(tài)會調(diào)用preemptone,來通過系統(tǒng)信號來完成搶占,之所以沒有在之前提及的原因在于該調(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.         // 對于 syscall 的情況,因為 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 標記為搶占 
  30.   gp.preempt = true 
  31.  
  32.   // 一個 Goroutine 中的每個調(diào)用都會通過比較當(dāng)前棧指針和 gp.stackgard0 
  33.   // 來檢查棧是否溢出。 
  34.   // 設(shè)置 gp.stackgard0 為 StackPreempt 來將搶占轉(zhuǎn)換為正常的棧溢出檢查。 
  35.   gp.stackguard0 = stackPreempt 
  36.  
  37.   // 請求該 P 的異步搶占 
  38.   if preemptMSupported && debug.asyncpreemptoff == 0 { 
  39.     _p_.preempt = true 
  40.     preemptM(mp) 
  41.   } 
  42.  
  43.   return true 

搶占信號的選取

preemptM 完成了信號的發(fā)送,其實現(xiàn)也非常直接,直接向需要進行搶占的 M 發(fā)送 SIGURG 信號即可。但是真正的重要的問題是,為什么是 SIGURG 信號而不是其他的信號?如何才能保證該信號不與用戶態(tài)產(chǎn)生的信號產(chǎn)生沖突?這里面有幾個原因:

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

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

  1. const sigPreempt = _SIGURG 
  2.  
  3. // preemptM 向 mp 發(fā)送搶占請求。該請求可以異步處理,也可以與對 M 的其他請求合并。 
  4. // 接收到該請求后,如果正在運行的 G 或 P 被標記為搶占,并且 Goroutine 處于異步安全點, 
  5. // 它將搶占 Goroutine。在處理搶占請求后,它始終以原子方式遞增 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)用的注入

我們在信號處理一節(jié)[5]中已經(jīng)知道,每個運行的 M 都會設(shè)置一個系統(tǒng)信號的處理的回調(diào),當(dāng)出現(xiàn)系統(tǒng)信號時,操作系統(tǒng)將負責(zé)將運行代碼進行中斷,并安全的保護其執(zhí)行現(xiàn)場,進而 Go 運行時能將針對信號的類型進行處理,當(dāng)信號處理函數(shù)執(zhí)行結(jié)束后,程序會再次進入內(nèi)核空間,進而恢復(fù)到被中斷的位置。

但是這里面有一個很巧妙的用法,因為 sighandler 能夠獲得操作系統(tǒng)所提供的執(zhí)行上下文參數(shù)(例如寄存器 rip, rep 等),如果在 sighandler 中修改了這個上下文參數(shù),OS 會根據(jù)就該的寄存器進行恢復(fù),這也就為搶占提供了機會。

  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.     // 可能是一個搶占信號 
  8.     doSigPreempt(gp, c) 
  9.     // 即便這是一個搶占信號,它也可能與其他信號進行混合,因此我們 
  10.     // 繼續(xù)進行處理。 
  11.   } 
  12.   ... 
  13. // doSigPreempt 處理了 gp 上的搶占信號 
  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 直接修改了這些寄存器,進而當(dāng)從 sighandler 返回用戶態(tài) Goroutine 時,能夠從注入的 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 棧幀時,將會保守的掃描調(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 時,會根據(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ā)送中斷信號(signalM(mp, sigPreempt))
  2. M2 收到信號,操作系統(tǒng)中斷其執(zhí)行代碼,并切換到信號處理函數(shù)(sighandler(signum, info, ctxt, gp))
  3. M2 修改執(zhí)行的上下文,并恢復(fù)到修改后的位置(asyncPreempt)
  4. 重新進入調(diào)度循環(huán)進而調(diào)度其他 Goroutine(preemptPark 和 gopreempt_m)

上述的異步搶占流程我們是通過系統(tǒng)監(jiān)控來說明的,正如前面所提及的,異步搶占的本質(zhì)是在為垃圾回收器服務(wù),由于我們還沒有討論過 Go 語言垃圾回收的具體細節(jié),這里便不做過多展開,讀者只需理解,在垃圾回收周期開始時,垃圾回收器將通過上述異步搶占的邏輯,停止所有用戶 Goroutine,進而轉(zhuǎn)去執(zhí)行垃圾回收。

小結(jié)

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

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

進一步閱讀的參考文獻[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 安全點分析: https://changkun.de/golang/zh-cn/part2runtime/ch08gc/safe[5]信號處理一節(jié): https://changkun.de/golang/zh-cn/part2runtime/ch06sched/signal/

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

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

2021-04-15 12:10:42

Go語言Go開發(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ù)字貨幣傳銷虛擬貨幣

2022-03-31 10:41:35

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

2018-06-07 13:17:12

契約測試單元測試API測試

2021-01-28 22:31:33

分組密碼算法

2020-05-22 08:16:07

PONGPONXG-PON

2023-09-22 17:36:37

2021-08-04 10:15:14

Go路徑語言

2021-01-01 09:01:05

前端組件化設(shè)計

2020-08-12 08:34:16

開發(fā)安全We

2022-10-08 11:33:56

邊緣計算云計算

2022-11-26 00:00:06

裝飾者模式Component

2020-06-28 09:30:37

Linux內(nèi)存操作系統(tǒng)

2022-03-08 16:10:38

Redis事務(wù)機制

2022-03-29 09:56:21

游戲版本運營

2018-01-10 14:13:04

測試矩陣API測試

2020-09-08 06:54:29

Java Gradle語言
點贊
收藏

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

日韩亚洲国产免费| 三级视频在线播放| 亚洲综合自拍| 亚洲成avwww人| 国产成人a亚洲精v品无码| 日韩av地址| 精彩视频一区二区| 97在线视频国产| www.涩涩爱| 久久九九热re6这里有精品| 91黄色激情网站| 亚洲精品一区二区三区av| 午夜精品久久久久久久99老熟妇 | 国产精品天干天干在线综合| 国产日本欧美视频| 日本天堂在线视频| 日韩欧美一区二区三区在线视频 | 可以在线看黄的网站| 日本毛片在线观看| 精品一区二区影视| 国自在线精品视频| 成年人二级毛片| 亚洲精品亚洲人成在线| 日韩午夜在线影院| 9l视频白拍9色9l视频| 2021中文字幕在线| 亚洲精品免费看| 久久精品五月婷婷| 精品国自产拍在线观看| 免费在线观看一区二区三区| 国语自产精品视频在线看| 日韩欧美综合视频| 欧美电影免费观看高清| 国产丝袜一区二区三区| 岛国精品一区二区三区| 91亚洲精品在看在线观看高清| 国产精品视频在线看| 久久久久高清| 亚洲美女综合网| 韩国av一区二区三区四区| 国产成人一区二| 日韩精品乱码久久久久久| 欧美日韩第一区| 欧美另类99xxxxx| 国精品无码一区二区三区| 热久久天天拍国产| 亚洲欧美日韩一区在线| 黄色短视频在线观看| www.豆豆成人网.com| 日韩视频一区二区在线观看| 手机精品视频在线| 欧美91在线|欧美| 欧美在线三级电影| 北条麻妃在线一区| 成人网ww555视频免费看| 欧美午夜精品久久久久久浪潮| 欧美日韩最好看的视频| 欧美老女人性开放| 久久亚洲精品国产精品紫薇| 欧美日韩在线播放一区二区| 欧美新色视频| 国产午夜亚洲精品午夜鲁丝片| 国产精品一二三在线| 波多野结衣黄色| 久久精品女人| 国产精品国语对白| 中文字幕男人天堂| 国产在线一区二区综合免费视频| 2019国产精品自在线拍国产不卡| 夜夜春很很躁夜夜躁| 精品日本12videosex| 一区二区三区视频免费| 毛片久久久久久| 久久久久国产精品| 欧美激情一二区| 91美女免费看| 日本亚洲免费观看| 91精品视频观看| 丰满肉嫩西川结衣av| 97久久久精品综合88久久| 久久久免费看| 国产二区视频在线观看| 成人免费在线播放视频| 国产成人生活片| 九九色在线视频| 激情av一区二区| 爱情岛论坛成人| 精品中文字幕一区二区三区| 精品噜噜噜噜久久久久久久久试看| 日韩一级免费在线观看| 国产精品久久久久久妇女| 欧美三电影在线| 精品人妻人人做人人爽夜夜爽| 日本成人在线网站| 精品国产一区二区在线观看| 少妇真人直播免费视频| 91精品亚洲| 久久久日本电影| 国产主播第一页| 国产福利视频一区二区三区| 欧美12av| av网址在线看| 色婷婷av一区二区三区之一色屋| 黄色www网站| 成人亚洲综合| 亚洲国产天堂久久综合网| 成人黄色a级片| 99国产精品视频免费观看一公开 | 日本在线视频网址| 狠狠躁18三区二区一区| 亚洲天堂av一区二区| 欧美大胆a级| 久久夜色撩人精品| 久久久蜜桃一区二区| 国产剧情一区在线| 日本高清不卡一区二区三| 毛片大全在线观看| 3atv在线一区二区三区| 野外性满足hd| 国产伊人精品| 91久久久国产精品| 国产高清一区在线观看| 亚洲国产一区二区三区| 中文字幕亚洲欧洲| 成人精品视频| 热久久这里只有精品| 亚洲精品久久久久久无码色欲四季| 国产98色在线|日韩| 日韩av一区二区三区在线观看| 亚乱亚乱亚洲乱妇| 欧美性猛交xxx| 一区二区免费在线观看视频| 在线观看国产精品入口| 国产精品高潮粉嫩av| 亚洲色图欧美视频| 午夜一区二区三区视频| 欧美激情一区二区三区p站| 99国内精品久久久久久久| 日韩av电影在线免费播放| 天天色综合久久| 亚洲一二三区在线观看| 国产又粗又猛又爽又黄| 亚洲在线久久| 亚洲free性xxxx护士hd| 黄色网页在线观看| 欧美日韩aaaaaa| 福利视频第一页| 久久精品国产免费看久久精品| 99中文字幕| 国产福利视频在线观看| 欧美欧美午夜aⅴ在线观看| 亚洲一级黄色录像| 青椒成人免费视频| 亚洲资源在线网| 欧美高清xxx| 日韩在线观看免费高清完整版| 久久高清无码视频| 国产成人一级电影| 国产成人永久免费视频| 91综合精品国产丝袜长腿久久| 亚洲天堂色网站| 中文字幕精品无| 亚洲国产精品激情在线观看| 污片在线免费看| 日韩免费视频| 亚洲伊人一本大道中文字幕| 色呦呦呦在线观看| 亚洲成人久久网| 精品国产一区二区三区四| 久久综合一区二区| 免费成年人高清视频| 亚洲综合色网| 精品蜜桃传媒| 精品成人av| 久久在线精品视频| 色欲久久久天天天综合网| 欧美日韩在线另类| 又嫩又硬又黄又爽的视频| 国产剧情一区二区三区| www.av中文字幕| 日韩av专区| 91大片在线观看| 中文字幕在线高清| xxxx欧美18另类的高清| 亚洲男人第一天堂| 91黄色免费观看| 2018天天弄| 久久欧美中文字幕| 国产精品久久久久久9999| 在线视频观看日韩| 亚洲免费精品视频| 91综合久久爱com| 国产精品91免费在线| 亚洲欧美成人影院| 亚洲欧洲国产伦综合| 国产xxxx在线观看| 欧美日韩国产精品| 三级av在线免费观看| av在线播放成人| 欧美激情国内自拍| 国产精品三上| ijzzijzzij亚洲大全| 五月国产精品| av观看久久| 成人精品国产| 午夜精品福利在线观看| 香蕉视频免费在线播放| 亚洲精品国产综合区久久久久久久 | 色播视频在线播放| 国产日韩欧美精品综合| 香蕉视频色在线观看| 久久九九电影| av在线观看地址| 亚洲一本二本| 午夜精品一区二区三区在线观看 | 红桃av永久久久| 国产aaaaaaaaa| 91亚洲精品久久久蜜桃| 91欧美一区二区三区| 日本亚洲最大的色成网站www| 日韩不卡av| 欧美第一在线视频| 国产精品一区久久久| 在线看片福利| 久久男人的天堂| 国产婷婷视频在线| 最新的欧美黄色| 国产小视频在线观看| 亚洲精品动漫久久久久| jlzzjlzzjlzz亚洲人| 欧美午夜精品一区二区三区| 亚洲日本韩国在线| 亚洲一区二区三区四区在线 | 一区二区三区国产好| 秋霞午夜一区二区| 免费在线小视频| 久久全球大尺度高清视频| av在线下载| 久久视频在线播放| 激情成人四房播| xxx欧美精品| 激情在线小视频| 久久综合九色九九| 蜜桃视频在线观看www社区| 中文字幕精品久久| av在线电影观看| 在线精品国产欧美| 日韩精品成人av| 日韩在线视频免费观看| 秋霞成人影院| 久久精品美女视频网站| 欧美69xxx| 久久成年人免费电影| a毛片在线播放| 欧美激情视频在线| 国产福利电影在线播放| 97热在线精品视频在线观看| 国产伦久视频在线观看| 欧美一级大胆视频| 国产高清不卡| 国产精品色午夜在线观看| 久久不卡日韩美女| 91亚洲精品视频| 4438全国亚洲精品观看视频| 国产一区二区高清不卡| 亚瑟一区二区三区四区| 欧美一区二区三区电影在线观看| 欧美大片91| 黑人另类av| 国产精品一区二区99| 神马影院我不卡午夜| 99视频精品视频高清免费| 欧美a级免费视频| 在线综合视频| 日本女优爱爱视频| 国产在线精品一区二区不卡了| 色综合av综合无码综合网站| 天堂一区二区在线| 手机精品视频在线| 99久久久免费精品国产一区二区| 一起操在线视频| 国产福利一区二区三区视频| 国产午夜在线一区二区三区| 久久久午夜电影| 天天操夜夜操av| 亚洲不卡av一区二区三区| 中文字幕av影院| 666欧美在线视频| 狠狠综合久久av一区二区| 亚洲人成网站777色婷婷| 电影在线一区| 欧美国产中文字幕| gogo亚洲高清大胆美女人体| 91九色精品视频| 亚洲欧美成人vr| 国产三级中文字幕| 亚洲欧美日韩一区在线观看| 日韩一级免费片| 北条麻妃一区二区三区| 蜜桃av免费观看| 亚洲成人自拍一区| 一级黄色短视频| 日韩高清不卡av| а天堂中文在线官网| 日本午夜在线亚洲.国产| 麻豆国产一区| 性欧美精品一区二区三区在线播放| 精品无人区麻豆乱码久久久| 日韩精品免费一区| 日韩影院在线观看| 欧美大喷水吹潮合集在线观看| 成人涩涩免费视频| 老司机深夜福利网站| 色综合婷婷久久| 成人午夜视频一区二区播放| 色偷偷噜噜噜亚洲男人| 在线看片国产福利你懂的| 97人人澡人人爽| 久久久久久久久久久9不雅视频| 手机成人av在线| 一区二区三区国产在线| 在线观看你懂的视频| 国产精品国产自产拍高清av王其| 久久久久久久久久97| 在线视频一区二区免费| 囯产精品久久久久久| 俺去啦;欧美日韩| 91福利精品在线观看| 鲁鲁狠狠狠7777一区二区| 欧美天天视频| 日本黄色www| 成人免费视频在线观看| 中文字幕有码视频| 一区二区三区视频观看| 欧美片第1页| 欧美1o一11sex性hdhd| 在线亚洲自拍| 极品粉嫩小仙女高潮喷水久久| 久久精品视频在线看| 日本三级小视频| 亚洲精品www久久久| 蜜臀av国内免费精品久久久夜夜| 91精品国产777在线观看| 大型av综合网站| 日本a在线天堂| 国产成人综合精品三级| 欧产日产国产v| 欧美岛国在线观看| 香蕉久久aⅴ一区二区三区| 亚洲综合日韩在线| 国产精品大片免费观看| 欧美激情一区二区三区p站| 亚洲成在人线在线播放| 天天躁日日躁狠狠躁喷水| 97视频国产在线| 欧美精品密入口播放| 91国视频在线| 久久久久久9999| 成人一二三四区| 自拍偷拍免费精品| 麻豆视频久久| 僵尸世界大战2 在线播放| 成年人午夜久久久| 东京热一区二区三区四区| 亚洲欧美日韩中文在线| 日本成人福利| 久久av秘一区二区三区| 国产成人av资源| 日韩免费在线视频观看| 精品亚洲夜色av98在线观看| 日韩伦理三区| 中文字幕久久综合| 国产成人av福利| 国产又黄又粗又爽| 在线观看日韩视频| 深夜福利一区二区三区| 欧美一区二区三区爽大粗免费| 国产成人aaa| 国产午夜性春猛交ⅹxxx| 在线观看欧美成人| 亚洲精品一区二区三区在线播放| 玖玖玖电影综合影院| 久久最新免费视频| 99视频超级精品| 久久久久久无码精品大片| 久久亚洲综合国产精品99麻豆精品福利| 欧美sm一区| 亚洲国产另类久久久精品极度| 亚洲伦理精品| 国产美女永久免费无遮挡| 51精品国自产在线| tube8在线hd| 亚洲成人一区二区三区| 国产99久久久精品| 免费黄色片视频| 欧美激情aaaa| 精品国产美女| 国产女人18毛片水真多18| 精品视频1区2区| 国产精品论坛|