相信接觸過 Go 語言的同窗,都應該有據說過 Go 協程,也就是 goroutine 的概念,對於 goroutine 的介紹,大部分文章中提到的都是,相較於線程,goroutine 十分輕量,相同大小的內存,能夠運行更多的 goroutine。可是不多有文章解釋 goroutine 是如何作到佔用更少資源的,單個 goroutine 究竟佔用多少內存?本文將針對這些問題進行解釋。程序員
聰明的你應該不難從上面這些結論中看出,goroutine 相較於線程更加輕量,關鍵點就在於棧空間的動態分配,這樣即可以最大限度的利用內存資源。既然是動態分配,那脫離實際狀況而單純說單個 goroutine 佔用多大內存,就有點吹毛求疵了。因此接下來,咱們就先來看看,goroutine 是如何作到棧空間動態分配的。安全
在 Go 的早期版本中,使用分段棧的方式進行內存管理,當一個goroutine被建立時,runtime 會爲協程分配 8KB 的內存區域。那麼問題來了,8KB 空間不夠了怎麼辦?bash
爲了解決這個問題,Go 會在每一個函數的入口處都插入一小段前置代碼,它可以檢查棧空間是否被消耗殆盡,若是用完了,便會調用 morestack() 函數來擴展空間。less
morestack()函數機理,即分段棧擴張機理:爲棧空間分配一塊新的內存區域。而後在這個新棧的底部的結構體中填充關於該棧的各類數據,包括剛剛來自的舊棧的地址。當獲得了一個新的棧分段以後,經過從新執行,致使棧被用完的函數,來重啓goroutine。這就被稱爲棧的分裂函數
+---------------+
| |
| unused |
| stack |
| space |
+---------------+
| test |
| |
+---------------+
| |
| lessstack |
+---------------+
| Stack info |
| |-----+
+---------------+ |
|
|
+---------------+ |
| test | |
| | <---+
+---------------+
| rest of stack |
| |
複製代碼
分段棧回溯機理:如上圖所示,新棧會爲lessstack()插入一個棧條目。這個函數並不實際顯式調用。它會在耗盡舊棧的那個函數返回的時候被設置,例如圖中的test(),當test()運行完畢返回時,會返回到lessstack()中,它會查詢棧底部的結構體信息,並調整棧指針(SP),以便可以回溯到上一個棧分段。而後,就能夠釋放新棧段空間了。ui
分段棧機制使得棧能夠按需擴張收縮。而程序員不須要在乎棧的大小。spa
可是分段棧也有瑕疵。收縮棧是一個相對昂貴的操做。若是是在一個循環中分裂棧狀況更明顯。函數會增加棧,分裂棧,返回棧,而且釋放棧分段。若是是在循環裏面作這些操做,那麼將會付出很大的開銷。例如循環一次經歷了這些過程,當下一次循環時棧又被耗盡,又得從新分配棧分段,而後又被釋放掉,周而復始,循環往復,開銷就會巨大。線程
這就是熟知的 hot split problem (熱點分裂問題)。這是Golang開發組切換到新的棧管理方式的主要緣由,新方式稱爲棧拷貝。指針
從GO1.4以後,開始正式使用了連續棧機制。rest
棧拷貝開始很像分段棧。協程運行,使用棧空間,當棧將要耗盡時,觸發相同的棧溢出檢測。
可是,不像分段棧裏有一個回溯連接,棧拷貝的方式則是建立了一個新的分段,它是舊棧的兩倍大小,而且把舊棧徹底拷貝進來。 這樣當棧收縮爲舊棧大小時,runtime不會作任何事情。收縮變成了一個no op免費操做。此外,當棧再次增加時,runtime也不須要作任何事情,從新使用剛纔擴容的空間便可。
不像聽起來那麼容易,其實拷貝棧是一項艱鉅的任務。因爲棧中的變量在Golang中可以獲取其地址,所以最終會出現指向棧的指針。而若是輕易拷貝移動棧,任何指向舊棧的指針都會失效。
而Golang的內存安全機制規定,任何可以指向棧的指針都必須存在於棧中。
因此能夠經過垃圾收集器協助棧拷貝,由於垃圾收集器須要知道哪些指針能夠進行回收,因此能夠查到棧上的哪些部分是指針,當進行棧拷貝時,會更新指針信息指向新目標,以及它相關的全部指針。
可是,runtime中大量核心調度函數和GC核心都是用C語言寫的,這些函數都獲取不到指針信息,那麼它們就沒法複製。這種都會在一個特殊的棧中執行,而且由runtime開發者分別定義棧尺寸。