棧空間管理的基本邏輯
go語言通過goroutine提供了併發程式設計支援,goroutine是go執行庫的功能,而不是作業系統執行緒實現的,goroutine可以被理解成一個使用者態的執行緒。
既然goroutine是由go執行庫管理的,那麼go執行庫也需要為每個goroutine建立並管理相應的棧空間,為每個goroutine分配的棧空間不能太大,goroutine開多時會浪費大量空間,也不能太小,會導致棧溢位。go語言選擇棧的棧空間管理的方式是,一開始給一個比較小的空間,隨著需要自動增長。當goroutine不需要那麼大的空間時,棧空間也要自動縮小。
分段棧 Segment Stacks
在go 1.3之前,go使用分段棧。
分段棧實現了一種不連續但是可以持續增長的棧,開始時,棧只有一個段,當需要更多的棧空間時,會分配一個新的段,和上一個棧雙向連結。這樣,一個棧就是由多個雙向連結的段所組成的。當新分配的段使用完畢後,新段會被釋放掉。
分段棧實現了棧的按需收縮,在增加新分段時也不需要對原有分段中的資料進行拷貝,使得goroutine的使用代價非常低廉。
分段棧的好處是可以按需增長,空間利用率比較高,然而分段棧在某些情況下也存在一定的瑕疵。當一個段即將用盡,這時使用for迴圈執行一個比較耗空間的函式,會導致函式執行時goroutine進行段的分配,而執行完成返回時,進行段的銷燬,這樣就會導致在迴圈中出現多次棧的擴容和收縮,造成很大的效能損失,這種情況被稱作棧分裂(Stack Split)。
連續棧 Contiguous Stacks
go 1.3推出了連續棧,連續棧使用了另外一種策略,不再把棧分成一段一段的,當棧空間不夠時,直接new一個2倍大的棧空間,並將原先棧空間中的資料拷貝到新的棧空間中,而後銷燬舊棧。這樣當出現棧空間觸及邊界時,不會產生棧分裂的情況。
繼續假設當前棧空間即將用盡,並且需要在for迴圈中執行一個比較消耗空間的函式。當該函式執行時,棧空間發生了擴容,變成原先2倍大小,函式執行完成一次後,棧空間的使用量縮小回執行前的大小,但是棧空間的使用量並沒有小於棧大小的1/4,不會觸發棧收縮,所以在整個for迴圈執行過程中,不會反覆觸發棧空間的收縮擴容。
總結
相比於分段棧,連續棧避免了某些場景下棧空間的的頻繁伸縮。有一點需要注意的是,連續棧的收縮也是需要重新申請一段空間(原先的1/2大小),並進行棧拷貝操作的。