Go 運(yùn)行時調(diào)度器處理系統(tǒng)調(diào)用的巧妙方式
goroutine[1] 是 Go 的一個標(biāo)志性特點(diǎn),是被 Go 運(yùn)行時所管理的輕量線程。Go 運(yùn)行時使用一個 M:N 工作竊取調(diào)度器[2]實(shí)現(xiàn) goroutine,將 Goroutine 復(fù)用在操作系統(tǒng)線程上。調(diào)度器有著特殊的術(shù)語用來描述三個重要的實(shí)體;G 是 goroutine,M 是 OS 線程(一個“機(jī)器 machine”),P 是“處理器(processor)”,它的核心是有限的資源,而 M 需要這些資源來運(yùn)行 Go 代碼。限制 P 的供應(yīng)是 Go 用來限制一次執(zhí)行多少操作以避免整個系統(tǒng)超載的手段。通常來說,每個 OS 所報(bào)告的實(shí)際的 CPU 有一個對應(yīng)的 P (P 的數(shù)量是 GOMAXPROCS[3])。
當(dāng) Goroutine 執(zhí)行 網(wǎng)絡(luò) IO 或者任何覺得可以異步完成的系統(tǒng)調(diào)用操作時,Go 有一個完整的運(yùn)行時子系統(tǒng),netpoller[4],(使用類似 epoll[5] 的系統(tǒng)調(diào)用機(jī)制)將看起來像多個單獨(dú)的同步操作轉(zhuǎn)換為一個單獨(dú)的等待。goroutine 并沒有真正進(jìn)行阻塞的系統(tǒng)調(diào)用,而是像等待一個 channel 就緒那樣進(jìn)入休眠狀態(tài)等待其網(wǎng)絡(luò)套接字。如果很難有效地實(shí)現(xiàn),概念上講這些都是直白的。
無論如何,網(wǎng)絡(luò) IO 以及類似的東西遠(yuǎn)不是 Go 程序可以處理的唯一的系統(tǒng)調(diào)用,因此 Go 也必須處理阻塞的系統(tǒng)調(diào)用。對 Goroutine 的 M 來說,處理阻塞的系統(tǒng)調(diào)用的直接方式是在系統(tǒng)調(diào)用前釋放 P ,并且在系統(tǒng)調(diào)用恢復(fù)后嘗試重新獲取 P 。如果那時候沒有空閑的 P ,goroutine 會隨著其他等待運(yùn)行的任務(wù)被停放在調(diào)度器中。
雖然理論上所有的系統(tǒng)調(diào)用都是阻塞的,在實(shí)踐中不是所有的調(diào)用都會阻塞。例如,在現(xiàn)代系統(tǒng)中,獲取當(dāng)前時間的“系統(tǒng)調(diào)用”可能甚至沒有進(jìn)入內(nèi)核(見 Linux 的 vdso(7)[6])。讓 Goroutine 完成釋放他們當(dāng)前的 P 的全部工作再為了這些系統(tǒng)調(diào)用重新獲取一個 P 有兩個問題:首先,所有涉及到的數(shù)據(jù)結(jié)構(gòu)的鎖定(和釋放)有著很大的開銷。其次,如果可運(yùn)行的 Goroutine 比 P 多,進(jìn)行這類系統(tǒng)調(diào)用的 Goroutine 無法重新獲取 P 并且不得不把自己停放;釋放 P 的瞬間,其他 Goroutine 就會被調(diào)度到上面。這是額外的運(yùn)行時開銷,有點(diǎn)不公平,并且不利于進(jìn)行快速系統(tǒng)調(diào)度的目的(尤其是那些不進(jìn)入內(nèi)核的調(diào)用)。
所以 Go 運(yùn)行時和調(diào)度器實(shí)際上有兩種處理阻塞系統(tǒng)調(diào)用的方法,一種悲觀方式,應(yīng)用于預(yù)計(jì)會很慢的系統(tǒng)調(diào)用;另一種樂觀方式,應(yīng)用于預(yù)計(jì)會很快的系統(tǒng)調(diào)用。悲觀的系統(tǒng)調(diào)用路徑實(shí)現(xiàn)了直接的方法,運(yùn)行時在系統(tǒng)調(diào)用前主動釋放 P,之后嘗試將 P 找回來,如果無法獲取則停放自身。樂觀的系統(tǒng)調(diào)用路徑不會釋放 P,相反,會設(shè)置一個特殊的 P 的狀態(tài)標(biāo)識并繼續(xù)進(jìn)行系統(tǒng)調(diào)用。一個特殊的內(nèi)部 goroutine,sysmon goroutine,定期執(zhí)行并尋找設(shè)置了這個“進(jìn)行系統(tǒng)調(diào)用中”狀態(tài)的時間太長了的 P,并將 P 從進(jìn)行系統(tǒng)調(diào)用的 Goroutine 那里偷走。當(dāng)系統(tǒng)調(diào)用返回,運(yùn)行時代碼檢查它的 P 是否被偷走,如果沒有則繼續(xù)執(zhí)行(如果 P 被偷走了的話,運(yùn)行時會嘗試獲取其他的 P,如果失敗可能會停放 goroutine)。
如果一切順利,樂觀的系統(tǒng)調(diào)用路徑有著非常低的開銷(大多數(shù)情況下,需要幾個原子比較和交換[7]操作)。如果不順利并且可運(yùn)行的 Goroutine 的數(shù)量比 P 多,一個 P 會有不必要的空閑,通常可能是數(shù)十微秒(sysmon Goroutine 最多每 20 微秒運(yùn)行一次,但如果似乎沒有必要的話可以減少運(yùn)行頻率)。可能存在著最壞的情況,但是一般來說,在 Go 運(yùn)行時方面這是一個值得的抉擇。
via: https://utcc.utoronto.ca/~cks/space/blog/programming/GoSchedulerAndSyscalls
作者:ChrisSiebenmann[8]譯者:dust347[9]校對:JYSDeveloper[10]
本文由 GCTT[11] 原創(chuàng)編譯,Go 中文網(wǎng)[12] 榮譽(yù)推出
參考資料
goroutine: https://tour.golang.org/concurrency/1
[2]一個 M:N 工作竊取調(diào)度器: https://rakyll.org/scheduler/
[3]GOMAXPROCS: https://golang.org/pkg/runtime/
[4]netpoller: https://morsmachine.dk/netpoller
[5]epoll: https://medium.com/@copyconstruct/the-method-to-epolls-madness-d9d2d6378642
[6]vdso(7): http://man7.org/linux/man-pages/man7/vdso.7.html
[7]原子比較和交換: https://en.wikipedia.org/wiki/Compare-and-swap
[8]ChrisSiebenmann: https://twitter.com/thatcks/
[9]dust347: https://github.com/dust347
[10]JYSDeveloper: https://github.com/JYSDeveloper
[11]GCTT: https://github.com/studygolang/GCTT
[12]Go 中文網(wǎng): https://studygolang.com/
推薦閱讀

