剖析使Go語言高效的5個特性(5/5): Goroutine的棧管理

翻譯原文鏈接? ?轉(zhuǎn)帖/轉(zhuǎn)載請注明出處

英文原文鏈接? ?發(fā)表于2014/06/07

Goroutine的棧管理

在上一篇文章里,我們已經(jīng)討論了goroutine減少了對上百個并發(fā)運行的線程的管理開銷。這里我們要在討論下goroutine的另外一個方面,它的棧管理。

下面是一個進程的內(nèi)存布局圖。這個圖里我們關心的是堆和棧的位置。

通常在進程的尋址空間里,堆是在內(nèi)存的底部,從程序的可執(zhí)行指令存儲空間(text)開始向上衍生。棧則位于虛擬地址空間的頂部,并向下衍生。

如果堆和棧衍生后相互覆蓋的話,那結(jié)果是災難性的。操作系統(tǒng)通常會在堆和棧之間設置一塊不可寫的內(nèi)存區(qū)域。如果堆和棧衍生到一起的話,程序就會退出。這塊內(nèi)存區(qū)域叫做保護頁(guard page)。它限制了進程的棧的大小。這個限制通常在幾M的量級。

我們前面講到過線程是共享尋址空間的。但是每個線程,它又必須要有它自己的棧。

因為很難預測一個線程需要多大的??臻g,很多的內(nèi)存空間都被預留給了線程的棧和保護頁以期望預留的??臻g足夠大,這樣保護頁不會被觸及到。這樣做的缺點就是隨著程序里線程數(shù)的增加,可用的尋址空間也相應地減少了。

我們已經(jīng)討論過Go的運行環(huán)境會把大量的goroutine在少數(shù)的線程上調(diào)度。那么這些goroutine的棧是怎么管理的呢?

Go語言沒有使用保護頁機制。Go的編譯器會在每個函數(shù)調(diào)用的時候插入一段代碼來檢查是否有足夠的??臻g來運行被調(diào)用的函數(shù)。如果空間不足,Go的運行環(huán)境就會分配更多的棧空間。因為有了這個檢查機制,一個goroutine的初始??梢院苄?。這樣Go程序員就可以把goroutine作為相對廉價的資源來使用。

下圖顯示了Go 1.2里是如何管理棧的。

當G調(diào)用H的時候,沒有足夠的??臻g來讓H運行,這時候Go運行環(huán)境就會從堆里分配一個新的棧內(nèi)存塊去讓H運行。在H返回到G之前,新分配的內(nèi)存塊被釋放回堆。這種管理棧的方法一般都工作得很好。但對有些代碼,特別是遞歸調(diào)用,它會造成程序不停地分配和釋放新的內(nèi)存空間。舉個例子,在一個程序里,函數(shù)G會在一個循環(huán)里調(diào)用很多次H函數(shù)。每次調(diào)用都會分配一塊新的內(nèi)存空間。這就是熱分裂問題(hot split problem)。

為了解決這個問題,Go 1.3采用了新的棧管理方法。

如果goroutine的棧太小了,它會去分配一塊新的更大的棧,而不是分配和釋法額外的內(nèi)存空間。老的棧里的內(nèi)容被復制到新的棧里,goroutine會在新的棧上繼續(xù)執(zhí)行。在第一次調(diào)用H函數(shù)之后,將會有足夠大的棧空間,這樣以后的??臻g大小檢查都不會有問題了。這就解決了熱分裂問題。

變量,內(nèi)聯(lián),逃逸分析,Goroutines,和棧的分塊/復制管理就是今天我要討論的5個特性。當然Go語言高效不僅僅是因為這5個特性。這就像大家有千奇百怪的理由來學習Go語言一樣,這些理由也絕不止3個。

這些特性個個都很有效,他們之間還相互依賴。比如,如果沒有了可衍生的棧,運行環(huán)境將多個goroutine復用到線程上面就不會很有效。內(nèi)聯(lián)在把多個小函數(shù)合并成大函數(shù)的時候也避免了棧大小的檢查開銷。逃逸分析用棧代替堆來存儲局部變量,這樣也減少來垃圾回收機制的壓力。逃逸分析還提升了緩存的性能(cache locality)。沒有可衍生的棧,逃逸分析又會對棧造成很大的壓力。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容