生生世世 —— schedule 的轮回(七)
上一講,我們講完 main goroutine 以及普通 goroutine 的退出過程。main goroutine 退出后直接調用 exit(0) 使得整個進程退出,而普通 goroutine 退出后,則進行了一系列的調用,最終又切到 g0 棧,執行 schedule 函數。
從前面的文章我們知道,普通 goroutine(gp)就是在 schedule 函數中被選中,然后才有機會執行。而現在,gp 執行完之后,再次進入 schedule 函數,形成一個循環。這個循環太長了,我們有必要再重新梳理一下。
如圖所示,rt0_go 負責 Go 程序啟動的所有初始化,中間進行了很多初始化工作,調用 mstart 之前,已經切換到了 g0 棧,圖中不同色塊表示使用不同的棧空間。
接著調用 gogo 函數,完成從 g0 棧到用戶 goroutine 棧的切換,包括 main goroutine 和普通 goroutine。
之后,執行 main 函數或者用戶自定義的 goroutine 任務。
執行完成后,main goroutine 直接調用 eixt(0) 退出,普通 goroutine 則調用 goexit -> goexit1 -> mcall,完成普通 goroutine 退出后的清理工作,然后切換到 g0 棧,調用 goexit0 函數,將普通 goroutine 添加到緩存池中,再調用 schedule 函數進行新一輪的調度。
schedule() -> execute() -> gogo() -> goroutine 任務 -> goexit() -> goexit1() -> mcall() -> goexit0() -> schedule()可以看出,一輪調度從調用 schedule 函數開始,經過一系列過程再次調用 schedule 函數來進行新一輪的調度,從一輪調度到新一輪調度的過程稱之為一個調度循環。
這里說的調度循環是指某一個工作線程的調度循環,而同一個Go 程序中存在多個工作線程,每個工作線程都在進行著自己的調度循環。
從前面的代碼分析可以得知,上面調度循環中的每一個函數調用都沒有返回,雖然 goroutine任務->goexit()->goexit1()->mcall() 是在 g2 的棧空間執行的,但剩下的函數都是在 g0 的棧空間執行的。
那么問題就來了,在一個復雜的程序中,調度可能會進行無數次循環,也就是說會進行無數次沒有返回的函數調用,大家都知道,每調用一次函數都會消耗一定的棧空間,而如果一直這樣無返回的調用下去無論 g0 有多少棧空間終究是會耗盡的,那么這里是不是有問題?其實沒有問題!關鍵點就在于,每次執行 mcall 切換到 g0 棧時都是切換到 g0.sched.sp 所指的固定位置,這之所以行得通,正是因為從 schedule 函數開始之后的一系列函數永遠都不會返回,所以重用這些函數上一輪調度時所使用過的棧內存是沒有問題的。
上面這三段引用自參考資料【阿波張 非 main goroutine 的退出及調度循環】。
我再解釋一下:棧空間在調用函數時會自動“增大”,而函數返回時,會自動“減小”,這里的增大和減小是指棧頂指針 SP 的變化。上述這些函數都沒有返回,說明調用者不需要用到被調用者的返回值,有點像“尾遞歸”。
因為 g0 一直沒有動過,所有它之前保存的 sp 還能繼續使用。每一次調度循環都會覆蓋上一次調度循環的棧數據,完美!
參考資料
【阿波張 非 main goroutine 的退出及調度循環】https://mp.weixin.qq.com/s/XttP9q7-PO7VXhskaBzGqA總結
以上是生活随笔為你收集整理的生生世世 —— schedule 的轮回(七)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 三足鼎立 —— GPM 到底是什么?(一
- 下一篇: 千难万险 —— goroutine 从生