溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Go語言中怎么對(duì)棧進(jìn)行處理

發(fā)布時(shí)間:2021-07-06 15:58:12 來源:億速云 閱讀:148 作者:Leah 欄目:web開發(fā)

本篇文章為大家展示了Go語言中怎么對(duì)棧進(jìn)行處理,內(nèi)容簡(jiǎn)明扼要并且容易理解,絕對(duì)能使你眼前一亮,通過這篇文章的詳細(xì)介紹希望你能有所收獲。

一、線程棧(thread stacks)介紹

在我們研究Go的棧處理方式之前,我們先來看看傳統(tǒng)語言,比如C是如何進(jìn)行棧管理的。

當(dāng)你啟動(dòng)一個(gè)C實(shí)現(xiàn)的thread時(shí),C標(biāo)準(zhǔn)庫會(huì)負(fù)責(zé)分配一塊內(nèi)存作為這個(gè)線程的棧。標(biāo)準(zhǔn)庫分配這塊內(nèi)存,告訴內(nèi)核它的位置并讓內(nèi)核處理這個(gè)線程 的執(zhí)行。不過當(dāng)這塊內(nèi)存不夠用時(shí),問題就來了,我們來看一下下面這個(gè)函數(shù):

int a(int m, int n) {     if (m == 0) {         return n + 1;     } else if (m > 0 && n == 0) {         return a(m – 1, 1);     } else {         return a(m – 1, a(m, n – 1));     } }

這個(gè)函數(shù)大量使用遞歸,執(zhí)行a(4, 5)就會(huì)降所有棧內(nèi)存耗盡。要解決這個(gè)問題,你可以調(diào)整標(biāo)準(zhǔn)庫給線程棧分配的內(nèi)存塊的大小。但是全線提高棧大小意味著每個(gè)線程都會(huì)提高棧的內(nèi)存使用量,即 便它們不是大量采用遞歸方式的。這樣一來,你將用光所有內(nèi)存,即便你的程序還尚未使用棧上的內(nèi)存。

另外一種可選的解決方法則是為每個(gè)線程單獨(dú)確定棧大小。這樣一來你就不得不完成這樣的任務(wù):根據(jù)每個(gè)線程的需要,估算它們的棧內(nèi)存的大小。這將是 創(chuàng)建線程的難度超出我們的期望。想搞清楚一般情況下一個(gè)線程棧需要多少內(nèi)存是不可行的,即便是通常情況也是非常困難的。

二、Go是如何應(yīng)對(duì)這個(gè)問題的

Go運(yùn)行時(shí)會(huì)試圖按需為goroutine提供它們所需要的??臻g,而不是為每個(gè)goroutine分配一個(gè)固定大小的棧空間。這樣可以把程序員 們從決定??臻g大小的煩心事中解脫了出來。不過Go核心團(tuán)隊(duì)正在嘗試切換到另外一種方案,這里我將嘗試闡述舊方案以及它的缺點(diǎn),新方案以及為何要 做出如此改變。

三、分段棧(Segmented Stacks)

分段棧(segmented stacks)是Go語言最初用來處理?xiàng)5姆桨?。?dāng)創(chuàng)建一個(gè)goroutine時(shí),Go運(yùn)行時(shí)會(huì)分配一段8K字節(jié)的內(nèi)存用于棧供goroutine運(yùn)行使 用,我們讓goroutine在這個(gè)棧上完成其任務(wù)處理。

當(dāng)我們用光這8K字節(jié)的??臻g后,問題隨之而來。為了解決這個(gè)問題,每個(gè)go函數(shù)在函數(shù)入口處都會(huì)有一小段代碼(called prologue),這段代碼會(huì)檢查是否用光了已分配的??臻g,如果用光了,這段代碼會(huì)調(diào)用morestack函數(shù)。

morestack函數(shù)會(huì)分配一段新內(nèi)存用作??臻g,接下來它會(huì)將有關(guān)棧的各種數(shù)據(jù)信息寫入棧底的一個(gè)struct中(譯注:下圖中Stack info),包括上一段棧的地址。有點(diǎn)我們擁有了一個(gè)新的棧段(stack segment),我們將重啟goroutine,從導(dǎo)致??臻g用光的那個(gè)函數(shù)(譯注:下圖中的Foobar)開始執(zhí)行。這就是所謂的“棧分裂 (stack split)”。

下面的棧示意圖剛好是我們進(jìn)行棧分裂后的情形:

Go語言中怎么對(duì)棧進(jìn)行處理

在新棧的底部,我們插入了一個(gè)棧入口函數(shù)lessstack。我們不會(huì)調(diào)用該函數(shù),設(shè)置這個(gè)函數(shù)就是用于我們從那個(gè)導(dǎo)致我們用光??臻g的函數(shù)(譯 注:Foobar)返回時(shí)用的。當(dāng)那個(gè)函數(shù)(譯注:Foobar)返回時(shí),我們回到lessstack(這個(gè)棧幀),lessstack會(huì)查找 stack底部的那個(gè)struct,并調(diào)整棧指針(stack pointer),使得我們返回到前一段??臻g。這樣做之后,我們就可以將這個(gè)新棧段(stack segment)釋放掉,并繼續(xù)執(zhí)行我們的程序了。

四、分段棧(Segmented stacks)的問題

分段棧給了我們具備按需伸縮能力的棧。程序員們無需擔(dān)心計(jì)算棧的大小了,啟動(dòng)一個(gè)新的goroutine代價(jià)低廉并且程序員不會(huì)知道棧將增長多 大。

這就是直到目前Go語言處理stack增長的方法,但是這個(gè)方法有個(gè)瑕疵。那就是??s小會(huì)是一個(gè)相對(duì)代價(jià)高昂的操作。如果你在一個(gè)循環(huán)遇到棧分裂 (stack split),你會(huì)最有感觸。一個(gè)函數(shù)會(huì)增加??臻g,做棧分裂,返回并釋放棧段(stack segment)。如果你在一個(gè)循環(huán)中進(jìn)行這些,你會(huì)付出很大的代價(jià)(性能方面)。

這就是所謂的“hot split”問題。它也是Go核心開發(fā)組更換到一個(gè)新的棧管理方案-??截?stack copying)的主要原因。

五、??截?stack copying)

??截惓跏茧A段與分段棧類似。goroutine在棧上運(yùn)行著,當(dāng)用光??臻g,它遇到與舊方案中相同的棧溢出檢查。但是與舊方案采用的保留一個(gè)返 回前一段棧的link不同,新方案創(chuàng)建一個(gè)兩倍于原stack大小的新stack,并將舊??截惖狡渲?。這意味著當(dāng)棧實(shí)際使用的空間縮小為原先的 大小時(shí),go運(yùn)行時(shí)不用做任何事情。??s小是一個(gè)無任何代價(jià)的操作。此外,當(dāng)棧再次增長時(shí),運(yùn)行時(shí)也無需做任何事情,我們只需要重用之前分配的空 閑空間即可。

六、棧是怎么拷貝的

拷貝棧聽起來簡(jiǎn)單,但實(shí)際上它是一件有難度的事情。因?yàn)镚o中棧上的變量都有自己的地址,一旦你擁有指向棧上變量的指針,這種情況下你就無法如你 所愿。當(dāng)你移動(dòng)棧時(shí),指向原棧的指針都將變?yōu)闊o效指針。

幸運(yùn)的是,只有在棧上分配的指針才能指向棧上的地址。這點(diǎn)對(duì)于內(nèi)存安全是極其必要的,否則,程序可能會(huì)訪問到已不再使用了的棧上的地址。

由于我們需要知道那些需要被垃圾收集器回收的指針的位置,因此我們知道棧上哪些部分是指針。當(dāng)我們移動(dòng)棧時(shí),我們可以更新棧里地指針使其指向新的 目標(biāo)地址,并且所有相關(guān)的指針都要被照顧到。

由于我們使用垃圾回收的信息來協(xié)助完成??截?,因此所有出現(xiàn)在棧上的函數(shù)都必須具備這些信息。但事情不總是這樣的。因?yàn)镚o運(yùn)行時(shí)的大部分代碼是 用C編寫的,大量的運(yùn)行時(shí)調(diào)用沒有指針信息可用,這樣就無法進(jìn)行拷貝。一旦這種情況發(fā)生,我們又不得不退回到分段棧方案,并接受為其付出的高昂代 價(jià)。

這就是當(dāng)前Go運(yùn)行時(shí)開發(fā)者大規(guī)模重寫Go runtime的原因。那些無法用Go重寫的代碼,比如調(diào)度器和垃圾收集器的內(nèi)核,將在一個(gè)特殊的棧上執(zhí)行,這個(gè)特殊棧的size由runtime開發(fā)者 單獨(dú)計(jì)算確定。

除了讓??截惓蔀榭赡苤?,這個(gè)方法還會(huì)使得我們?cè)谖磥砟軌驅(qū)崿F(xiàn)出并發(fā)垃圾回收等特性。

七、關(guān)于虛擬內(nèi)存

另外一種不同的棧處理方式就是在虛擬內(nèi)存中分配大內(nèi)存段。由于物理內(nèi)存只是在真正使用時(shí)才會(huì)被分配,因此看起來好似你可以分配一個(gè)大內(nèi)存段并讓操 作系統(tǒng)處理它。下面是這種方法的一些問題

首先,32位系統(tǒng)只能支持4G字節(jié)虛擬內(nèi)存,并且應(yīng)用只能用到其中的3G空間。由于同時(shí)運(yùn)行百萬goroutines的情況并不少見,因此你很可 能用光虛擬內(nèi)存,即便我們假設(shè)每個(gè)goroutine的stack只有8K。

第二,然而我們可以在64位系統(tǒng)中分配大內(nèi)存,它依賴于過量?jī)?nèi)存使用。所謂過量使用是指當(dāng)你分配的內(nèi)存大小超出物理內(nèi)存大小時(shí),依賴操作系統(tǒng)保證 在需要時(shí)能夠分配出物理內(nèi)存。然而,允許過量使用可能會(huì)導(dǎo)致一些風(fēng)險(xiǎn)。由于一些進(jìn)程分配了超出機(jī)器物理內(nèi)存大小的內(nèi)存,如果這些進(jìn)程使用更多內(nèi)存 時(shí),操作系統(tǒng)將不得不為它們補(bǔ)充分配內(nèi)存。這會(huì)導(dǎo)致操作系統(tǒng)將一些內(nèi)存段放入磁盤緩存,這常常會(huì)增加不可預(yù)測(cè)的處理延遲。正是考慮到這個(gè)原因,一 些新系統(tǒng)關(guān)閉了對(duì)過量使用的支持。

上述內(nèi)容就是Go語言中怎么對(duì)棧進(jìn)行處理,你們學(xué)到知識(shí)或技能了嗎?如果還想學(xué)到更多技能或者豐富自己的知識(shí)儲(chǔ)備,歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI