1. 程式人生 > 其它 >Golang搶佔式排程

Golang搶佔式排程

Golang搶佔式排程

在1.12版本之前,go的排程器不支援搶佔式排程,程式只能依靠Goroutine主動讓出CPU資源才能觸發排程,會引發一些問題,如

  • 某些Goroutine可以長時間佔用執行緒,造成其它 Goroutine的飢餓
  • 垃圾回收器是需要stop the world的。如果垃圾回收器想要運行了,那麼它必須先通知其它的goroutine合作停下來,這會造成較長時間的等待時間。

基於協作的搶佔式排程

Goroutine引入了stackguard0欄位,當該欄位設定為StackPreempt意味著當前Goroutine發出了搶佔請求;同時在runtime.newstack:1e112cd中增加搶佔的程式碼,當stackguard0等於StackPreempt時觸發排程器搶佔讓出執行緒。

gorogoutine建立之初,棧的大小是固定的,為了防止棧溢位,編譯器會在有明顯棧消耗的函式頭部插入一些檢測程式碼,通過stackguard0值來決定是否觸發runtime.morestack函式。將stackguard0設定為StackPreempt作用是進入函式時必定觸發runtime.morestack,然後在呼叫runtime.newstack。

工作機制

  • 編譯器會在呼叫函式前插入runtime.morestack,可能會呼叫runtime.newstack進行搶佔
  • Go語言執行時會在垃圾回收暫停程式、系統監控發現Goroutine執行超過10ms時發出搶佔請求StackPreempt
  • 當發生函式呼叫時,可能會執行編譯器插入的runtime.morestack,它呼叫的runtime.newstack會檢查Goroutine的stackguard0欄位是否為 StackPreempt;
  • 如果stackguard0是StackPreempt,就會觸發搶佔讓出當前執行緒;

但是這種排程並不完備,比如一個goroutine運行了很久,但是它並沒有呼叫另一個函式,則它不會被搶佔。

基於訊號的搶佔式排程

在之前的依賴棧增長檢測程式碼的方式,遇到沒有函式呼叫的情況下就會出現問題,在Go1.14這一問題得到解決。
在Linux中這種真正的搶佔式排程是基於訊號完成的,所以也稱為“非同步搶佔”。

工作機制

  • M註冊一個SIGURG訊號的處理函式:sighandler。
  • sysmon執行緒檢測到執行時間過長的goroutine或者GC stw時,會向相應的M(或者說執行緒,每個執行緒對應一個 M)傳送SIGURG訊號。
  • 收到訊號後,核心執行sighandler函式,通過pushCall插入asyncPreempt函式呼叫。
  • 回到當前goroutine執行asyncPreempt函式,通過mcall切到g0棧執行gopreempt_m。
  • 將當前goroutine插入到全域性可執行佇列,M則繼續尋找其他goroutine來執行。
  • 被搶佔的goroutine再次排程過來執行時,會繼續原來的執行流。

基於訊號的搶佔式排程,搶佔也只會在垃圾回收掃描任務時觸發。