go語言heap go語言和php 區(qū)別

Golang實驗性功能SetMaxHeap 固定值GC

簡單來說, SetMaxHeap 提供了一種可以設(shè)置固定觸發(fā)閾值的 GC (Garbage Collection垃圾回收)方式

創(chuàng)新互聯(lián)專注于企業(yè)營銷型網(wǎng)站、網(wǎng)站重做改版、張家界網(wǎng)站定制設(shè)計、自適應(yīng)品牌網(wǎng)站建設(shè)、H5技術(shù)、購物商城網(wǎng)站建設(shè)、集團公司官網(wǎng)建設(shè)、外貿(mào)網(wǎng)站制作、高端網(wǎng)站制作、響應(yīng)式網(wǎng)頁設(shè)計等建站業(yè)務(wù),價格優(yōu)惠性價比高,為張家界等各大城市提供網(wǎng)站開發(fā)制作服務(wù)。

官方源碼鏈接

大量臨時對象分配導(dǎo)致的 GC 觸發(fā)頻率過高, GC 后實際存活的對象較少,

或者機器內(nèi)存較充足,希望使用剩余內(nèi)存,降低 GC 頻率的場景

GC 會 STW ( Stop The World ),對于時延敏感場景,在一個周期內(nèi)連續(xù)觸發(fā)兩輪 GC ,那么 STW 和 GC 占用的 CPU 資源都會造成很大的影響, SetMaxHeap 并不一定是完美的,在某些場景下做了些權(quán)衡,官方也在進行相關(guān)的實驗,當前方案仍沒有合入主版本。

先看下如果沒有 SetMaxHeap ,對于如上所述的場景的解決方案

這里簡單說下 GC 的幾個值的含義,可通過 GODEBUG=gctrace=1 獲得如下數(shù)據(jù)

這里只關(guān)注 128-132-67 MB 135 MB goal ,

分別為 GC開始時內(nèi)存使用量 - GC標記完成時內(nèi)存使用量 - GC標記完成時的存活內(nèi)存量 本輪GC標記完成時的 預(yù)期 內(nèi)存使用量(上一輪 GC 完成時確定)

引用 GC peace設(shè)計文檔 中的一張圖來說明

對應(yīng)關(guān)系如下:

簡單說下 GC pacing (信用機制)

GC pacing 有兩個目標,

那么當一輪 GC 完成時,如何只根據(jù)本輪 GC 存活量去實現(xiàn)這兩個小目標呢?

這里實際是根據(jù)當前的一些數(shù)據(jù)或狀態(tài)去 預(yù)估 “未來”,所有會存在些誤差

首先確定 gc Goal goal = memstats.heap_marked + memstats.heap_marked*uint64(gcpercent)/100

heap_marked 為本輪 GC 存活量, gcpercent 默認為 100 ,可以通過環(huán)境變量 GOGC=100 或者 debug.SetGCPercent(100) 來設(shè)置

那么默認情況下 goal = 2 * heap_marked

gc_trigger 是與 goal 相關(guān)的一個值( gc_trigger 大約為 goal 的 90% 左右),每輪 GC 標記完成時,會根據(jù) |Ha-Hg| 和實際使用的 cpu 資源 動態(tài)調(diào)整 gc_trigger 與 goal 的差值

goal 與 gc_trigger 的差值即為,為 GC 期間分配的對象所預(yù)留的空間

GC pacing 還會預(yù)估下一輪 GC 發(fā)生時,需要掃描對象對象的總量,進而換算為下一輪 GC 所需的工作量,進而計算出 mark assist 的值

本輪 GC 觸發(fā)( gc_trigger ),到本輪的 goal 期間,需要盡力完成 GC mark 標記操作,所以當 GC 期間,某個 goroutine 分配大量內(nèi)存時,就會被拉去做 mark assist 工作,先進行 GC mark 標記賺取足夠的信用值后,才能分配對應(yīng)大小的對象

根據(jù)本輪 GC 存活的內(nèi)存量( heap_marked )和下一輪 GC 觸發(fā)的閾值( gc_trigger )計算 sweep assist 的值,本輪 GC 完成,到下一輪 GC 觸發(fā)( gc_trigger )時,需要盡力完成 sweep 清掃操作

預(yù)估下一輪 GC 所需的工作量的方式如下:

繼續(xù)分析文章開頭的問題,如何充分利用剩余內(nèi)存,降低 GC 頻率和 GC 對 CPU 的資源消耗

如上圖可以看出, GC 后,存活的對象為 2GB 左右,如果將 gcpercent 設(shè)置為 400 ,那么就可以將下一輪 GC 觸發(fā)閾值提升到 10GB 左右

前面一輪看起來很好,提升了 GC 觸發(fā)的閾值到 10GB ,但是如果某一輪 GC 后的存活對象到達 2.5GB 的時候,那么下一輪 GC 觸發(fā)的閾值,將會超過內(nèi)存閾值,造成 OOM ( Out of Memory ),進而導(dǎo)致程序崩潰。

可以通過 GOGC=off 或者 debug.SetGCPercent(-1) 來關(guān)閉 GC

可以通過進程外監(jiān)控內(nèi)存使用狀態(tài),使用信號觸發(fā)的方式通知程序,或 ReadMemStats 、或 linkname runtime.heapRetained 等方式進行堆內(nèi)存使用的監(jiān)測

可以通過調(diào)用 runtime.GC() 或者 debug.FreeOSMemory() 來手動進行 GC 。

這里還需要說幾個事情來解釋這個方案所存在的問題

通過 GOGC=off 或者 debug.SetGCPercent(-1) 是如何關(guān)閉 GC 的?

gc 4 @1.006s 0%: 0.033+5.6+0.024 ms clock, 0.27+4.4/11/25+0.19 ms cpu, 428-428-16 MB, 17592186044415 MB goal, 8 P (forced)

通過 GC trace 可以看出,上面所說的 goal 變成了一個很詭異的值 17592186044415

實際上關(guān)閉 GC 后, Go 會將 goal 設(shè)置為一個極大值 ^uint64(0) ,那么對應(yīng)的 GC 觸發(fā)閾值也被調(diào)成了一個極大值,這種處理方式看起來也沒什么問題,將閾值調(diào)大,預(yù)期永遠不會再觸發(fā) GC

那么如果在關(guān)閉 GC 的情況下,手動調(diào)用 runtime.GC() 會導(dǎo)致什么呢?

由于 goal 和 gc_trigger 被設(shè)置成了極大值, mark assist 和 sweep assist 也會按照這個錯誤的值去計算,導(dǎo)致工作量預(yù)估錯誤,這一點可以從 trace 中進行證明

可以看到很詭異的 trace 圖,這里不做深究,該方案與 GC pacing 信用機制不兼容

記住,不要在關(guān)閉 GC 的情況下手動觸發(fā) GC ,至少在當前 Go1.14 版本中仍存在這個問題

SetMaxHeap 的實現(xiàn)原理,簡單來說是強行控制了 goal 的值

注: SetMaxHeap ,本質(zhì)上是一個軟限制,并不能解決 極端場景 下的 OOM ,可以配合內(nèi)存監(jiān)控和 debug.FreeOSMemory() 使用

SetMaxHeap 控制的是堆內(nèi)存大小, Go 中除了堆內(nèi)存還分配了如下內(nèi)存,所以實際使用過程中,與實際硬件內(nèi)存閾值之間需要留有一部分余量。

對于文章開始所述問題,使用 SetMaxHeap 后,預(yù)期的 GC 過程大概是這個樣子

簡單用法1

該方法簡單粗暴,直接將 goal 設(shè)置為了固定值

注:通過上文所講,觸發(fā) GC 實際上是 gc_trigger ,所以當閾值設(shè)置為 12GB 時,會提前一點觸發(fā) GC ,這里為了描述方便,近似認為 gc_trigger=goal

簡單用法2

當不關(guān)閉 GC 時, SetMaxHeap 的邏輯是, goal 仍按照 gcpercent 進行計算,當 goal 小于 SetMaxHeap 閾值時不進行處理;當 goal 大于 SetMaxHeap 閾值時,將 goal 限制為 SetMaxHeap 閾值

注:通過上文所講,觸發(fā) GC 實際上是 gc_trigger ,所以當閾值設(shè)置為 12GB 時,會提前一點觸發(fā) GC ,這里為了描述方便,近似認為 gc_trigger=goal

切換到 go1.14 分支,作者選擇了 git checkout go1.14.5

選擇官方提供的 cherry-pick 方式(可能需要梯子,文件改動不多,我后面會列出具體改動)

git fetch "" refs/changes/67/227767/3 git cherry-pick FETCH_HEAD

需要重新編譯Go源碼

注意點:

下面源碼中的官方注釋說的比較清楚,在一些關(guān)鍵位置加入了中文注釋

入?yún)ytes為要設(shè)置的閾值

notify 簡單理解為 GC 的策略 發(fā)生變化時會向 channel 發(fā)送通知,后續(xù)源碼可以看出“策略”具體指哪些內(nèi)容

返回值為本次設(shè)置之前的 MaxHeap 值

$GOROOT/src/runtime/debug/garbage.go

$GOROOT/src/runtime/mgc.go

注:作者盡量用通俗易懂的語言去解釋 Go 的一些機制和 SetMaxHeap 功能,可能有些描述與實現(xiàn)細節(jié)不完全一致,如有錯誤還請指出

Go 語言中如何使用堆 Heap

Go 提供了 container/heap 這個包來實現(xiàn)堆的操作。堆實際上是一個樹的結(jié)構(gòu),每個元素的值都是它的子樹中最小的,因此根節(jié)點 index = 0 的值是最小的,即最小堆。

堆也是實現(xiàn)優(yōu)先隊列 Priority Queue 的常用方式。

堆中元素的類型需要實現(xiàn) heap.Interface 這個接口:

其中 sort.Interface 包括 Len() , Less , Swap 方法:

一個完整的示例如下:

注意注釋中的一句話 Push 和 Pop 方法需要使用指針,因為它們會修改 slice 的長度,而不僅僅只內(nèi)容 。

Leetcode 692. Top K Frequent Words 也可以使用 Go 語言通過構(gòu)造 PriorityQueue 來實現(xiàn):

Golang的pprof的使用心得(CPU,Heap)

參照的是 這個文章

首先自己寫一段demo

里面負責(zé)2件事

doSomeThingOne

genSomeBytes

運行這個程序go run main.go

To install thewrk,you need only:

git clone

cd wrk

make

wrk relies on the openssl and luajit, learn more from its github page

Generating requests

Our demo is listening on the port 9876 ,so let's generate some requests for that.

./wrk -c400 -t8 -d5m

-c400means we have 400 connections to keep open

-t8means we use 8 threads to build requests

-d5mmeans the duration of the test will last for 5 minutes

用這段命令來壓服務(wù)器

Our server is very busy now and we can see some information via browser. Inputlocalhost:9876/debug/pprofyou will see:

然后用命令進入

在這里能看見各種方法的運行時間

所以我們安裝Graphviz 在mac下

brew install graphviz

之后再這個(pprof)里面輸入web

會生產(chǎn)一個svg文件

用瀏覽器打開我們就會看到

很顯然gensomebytes里面的math方法最消耗時間。這個就是我們優(yōu)化的對象

其實也很方便在

localhost:9876/debug/pprof/profile改成

localhost:9876/debug/pprof/heap

后面的結(jié)果一樣。。和cpu一樣可以看到那個heap占用了大量的內(nèi)存到時候優(yōu)化吧

這個文章里面的第一個方法就可以做測試內(nèi)存占用的.

有空試試把

分布式程序 A B C D 4個進程在服務(wù)器. 監(jiān)控程序E 打包程序F

寫一個監(jiān)控程序定時監(jiān)控這4個進程的CPU 內(nèi)存(搞成配置文件)

達到性能瓶頸(例如 90%CPU 內(nèi)存剩下10%)? E用shell觸發(fā)打包程序F把pprof等信息打包.并發(fā)送郵件

給配置者.

container之heap

go 的 heap 實現(xiàn)了堆,關(guān)于堆可以看下 數(shù)據(jù)結(jié)構(gòu):堆(Heap) ,這里就不闡述了,go實現(xiàn)的源碼在 container/heap/heap.go 中,其中包含了1個接口,5個外部方法和2個內(nèi)部方法

也就是說,要使用 heap ,需要自己實現(xiàn) heap.Interface 接口,并不是開箱即用的,除了 Less 方法,其他的都能根據(jù)方法名推出功能, Less 方法返回 bool 類型,典型的實現(xiàn)就是兩個元素的比較,通過更改比較規(guī)則,我們可以很方便的實現(xiàn)最大堆和最小堆

down 和 up ,根據(jù)自定義的 Less 和 Swap 方法進行比較和調(diào)換,一個實現(xiàn)向下追溯,一個實現(xiàn)向上追溯,最終目的是為了保證每一個父節(jié)點、左子節(jié)點和右子節(jié)點的三元組滿足 父節(jié)點 =/= min/max(左子節(jié)點,右子節(jié)點)

源碼分析的差不多了,下面來看下應(yīng)用

heap 個人感覺是一個工具類的半成品吧,做不到開箱即用,而且使用者需要對 heap 的實現(xiàn)比較了解,否則可能容易踩坑,比如上面說到的, heap 中 Pop 方法默認彈出末位元素, Less 方法如何實現(xiàn)才能控制最大/小堆等等,而且使用其他方法之前必須先調(diào)用 Init 方法,否則結(jié)果可能是非預(yù)期的,這點 heap 里面也沒有進行限制,最后建議如果使用heap,最好跟上面例子一樣使用slice進行包裝,這樣接入的成本是最低的

Go 語言內(nèi)存管理(三):逃逸分析

Go 語言較之 C 語言一個很大的優(yōu)勢就是自帶 GC 功能,可 GC 并不是沒有代價的。寫 C 語言的時候,在一個函數(shù)內(nèi)聲明的變量,在函數(shù)退出后會自動釋放掉,因為這些變量分配在棧上。如果你期望變量的數(shù)據(jù)可以在函數(shù)退出后仍然能被訪問,就需要調(diào)用 malloc 方法在堆上申請內(nèi)存,如果程序不再需要這塊內(nèi)存了,再調(diào)用 free 方法釋放掉。Go 語言不需要你主動調(diào)用 malloc 來分配堆空間,編譯器會自動分析,找出需要 malloc 的變量,使用堆內(nèi)存。編譯器的這個分析過程就叫做逃逸分析。

所以你在一個函數(shù)中通過 dict := make(map[string]int) 創(chuàng)建一個 map 變量,其背后的數(shù)據(jù)是放在??臻g上還是堆空間上,是不一定的。這要看編譯器分析的結(jié)果。

可逃逸分析并不是百分百準確的,它有缺陷。有的時候你會發(fā)現(xiàn)有些變量其實在??臻g上分配完全沒問題的,但編譯后程序還是把這些數(shù)據(jù)放在了堆上。如果你了解 Go 語言編譯器逃逸分析的機制,在寫代碼的時候就可以有意識地繞開這些缺陷,使你的程序更高效。

Go 語言雖然在內(nèi)存管理方面降低了編程門檻,即使你不了解堆棧也能正常開發(fā),但如果你要在性能上較真的話,還是要掌握這些基礎(chǔ)知識。

這里不對堆內(nèi)存和棧內(nèi)存的區(qū)別做太多闡述。簡單來說就是, 棧分配廉價,堆分配昂貴。 ??臻g會隨著一個函數(shù)的結(jié)束自動釋放,堆空間需要時間 GC 模塊不斷地跟蹤掃描回收。如果對這兩個概念有些迷糊,建議閱讀下面 2 個文章:

這里舉一個小例子,來對比下堆棧的差別:

stack 函數(shù)中的變量 i 在函數(shù)退出會自動釋放;而 heap 函數(shù)返回的是對變量 i 的引用,也就是說 heap() 退出后,表示變量 i 還要能被訪問,它會自動被分配到堆空間上。

他們編譯出來的代碼如下:

邏輯的復(fù)雜度不言而喻,從上面的匯編中可看到, heap() 函數(shù)調(diào)用了 runtime.newobject() 方法,它會調(diào)用 mallocgc 方法從 mcache 上申請內(nèi)存,申請的內(nèi)部邏輯前面文章已經(jīng)講述過。堆內(nèi)存分配不僅分配上邏輯比棧空間分配復(fù)雜,它最致命的是會帶來很大的管理成本,Go 語言要消耗很多的計算資源對其進行標記回收(也就是 GC 成本)。

Go 編輯器會自動幫我們找出需要進行動態(tài)分配的變量,它是在編譯時追蹤一個變量的生命周期,如果能確認一個數(shù)據(jù)只在函數(shù)空間內(nèi)訪問,不會被外部使用,則使用??臻g,否則就要使用堆空間。

我們在 go build 編譯代碼時,可使用 -gcflags '-m' 參數(shù)來查看逃逸分析日志。

以上面的兩個函數(shù)為例,編譯的日志輸出是:

日志中的 i escapes to heap 表示該變量數(shù)據(jù)逃逸到了堆上。

需要使用堆空間,所以逃逸,這沒什么可爭議的。但編譯器有時會將 不需要 使用堆空間的變量,也逃逸掉。這里是容易出現(xiàn)性能問題的大坑。網(wǎng)上有很多相關(guān)文章,列舉了一些導(dǎo)致逃逸情況,其實總結(jié)起來就一句話:

多級間接賦值容易導(dǎo)致逃逸 。

這里的多級間接指的是,對某個引用類對象中的引用類成員進行賦值。Go 語言中的引用類數(shù)據(jù)類型有 func , interface , slice , map , chan , *Type(指針) 。

記住公式 Data.Field = Value ,如果 Data , Field 都是引用類的數(shù)據(jù)類型,則會導(dǎo)致 Value 逃逸。這里的等號 = 不單單只賦值,也表示參數(shù)傳遞。

根據(jù)公式,我們假設(shè)一個變量 data 是以下幾種類型,相應(yīng)的可以得出結(jié)論:

下面給出一些實際的例子:

如果變量值是一個函數(shù),函數(shù)的參數(shù)又是引用類型,則傳遞給它的參數(shù)都會逃逸。

上例中 te 的類型是 func(*int) ,屬于引用類型,參數(shù) *int 也是引用類型,則調(diào)用 te(j) 形成了為 te 的參數(shù)(成員) *int 賦值的現(xiàn)象,即 te.i = j 會導(dǎo)致逃逸。代碼中其他幾種調(diào)用都沒有形成 多級間接賦值 情況。

同理,如果函數(shù)的參數(shù)類型是 slice , map 或 interface{} 都會導(dǎo)致參數(shù)逃逸。

匿名函數(shù)的調(diào)用也是一樣的,它本質(zhì)上也是一個函數(shù)變量。有興趣的可以自己測試一下。

只要使用了 Interface 類型(不是 interafce{} ),那么賦值給它的變量一定會逃逸。因為 interfaceVariable.Method() 先是間接的定位到它的實際值,再調(diào)用實際值的同名方法,執(zhí)行時實際值作為參數(shù)傳遞給方法。相當于 interfaceVariable.Method.this = realValue

向 channel 中發(fā)送數(shù)據(jù),本質(zhì)上就是為 channel 內(nèi)部的成員賦值,就像給一個 slice 中的某一項賦值一樣。所以 chan *Type , chan map[Type]Type , chan []Type , chan interface{} 類型都會導(dǎo)致發(fā)送到 channel 中的數(shù)據(jù)逃逸。

這本來也是情理之中的,發(fā)送給 channel 的數(shù)據(jù)是要與其他函數(shù)分享的,為了保證發(fā)送過去的指針依然可用,只能使用堆分配。

可變參數(shù)如 func(arg ...string) 實際與 func(arg []string) 是一樣的,會增加一層訪問路徑。這也是 fmt.Sprintf 總是會使參數(shù)逃逸的原因。

例子非常多,這里不能一一列舉,我們只需要記住分析方法就好,即,2 級或更多級的訪問賦值會 容易 導(dǎo)致數(shù)據(jù)逃逸。這里加上 容易 二字是因為隨著語言的發(fā)展,相信這些問題會被慢慢解決,但現(xiàn)階段,這個可以作為我們分析逃逸現(xiàn)象的依據(jù)。

下面代碼中包含 2 種很常規(guī)的寫法,但他們卻有著很大的性能差距,建議自己想下為什么。

Benchmark 和 pprof 給出的結(jié)果:

熟悉堆棧概念可以讓我們更容易看透 Go 程序的性能問題,并進行優(yōu)化。

多級間接賦值會導(dǎo)致 Go 編譯器出現(xiàn)不必要的逃逸,在一些情況下可能我們只需要修改一下數(shù)據(jù)結(jié)構(gòu)就會使性能有大幅提升。這也是很多人不推薦在 Go 中使用指針的原因,因為它會增加一級訪問路徑,而 map , slice , interface{} 等類型是不可避免要用到的,為了減少不必要的逃逸,只能拿指針開刀了。

大多數(shù)情況下,性能優(yōu)化都會為程序帶來一定的復(fù)雜度。建議實際項目中還是怎么方便怎么寫,功能完成后通過性能分析找到瓶頸所在,再對局部進行優(yōu)化。

【golang詳解】go語言GMP(GPM)原理和調(diào)度

Goroutine調(diào)度是一個很復(fù)雜的機制,下面嘗試用簡單的語言描述一下Goroutine調(diào)度機制,想要對其有更深入的了解可以去研讀一下源碼。

首先介紹一下GMP什么意思:

G ----------- goroutine: 即Go協(xié)程,每個go關(guān)鍵字都會創(chuàng)建一個協(xié)程。

M ---------- thread內(nèi)核級線程,所有的G都要放在M上才能運行。

P ----------- processor處理器,調(diào)度G到M上,其維護了一個隊列,存儲了所有需要它來調(diào)度的G。

Goroutine 調(diào)度器P和 OS 調(diào)度器是通過 M 結(jié)合起來的,每個 M 都代表了 1 個內(nèi)核線程,OS 調(diào)度器負責(zé)把內(nèi)核線程分配到 CPU 的核上執(zhí)行

模型圖:

避免頻繁的創(chuàng)建、銷毀線程,而是對線程的復(fù)用。

1)work stealing機制

當本線程無可運行的G時,嘗試從其他線程綁定的P偷取G,而不是銷毀線程。

2)hand off機制

當本線程M0因為G0進行系統(tǒng)調(diào)用阻塞時,線程釋放綁定的P,把P轉(zhuǎn)移給其他空閑的線程執(zhí)行。進而某個空閑的M1獲取P,繼續(xù)執(zhí)行P隊列中剩下的G。而M0由于陷入系統(tǒng)調(diào)用而進被阻塞,M1接替M0的工作,只要P不空閑,就可以保證充分利用CPU。M1的來源有可能是M的緩存池,也可能是新建的。當G0系統(tǒng)調(diào)用結(jié)束后,根據(jù)M0是否能獲取到P,將會將G0做不同的處理:

如果有空閑的P,則獲取一個P,繼續(xù)執(zhí)行G0。

如果沒有空閑的P,則將G0放入全局隊列,等待被其他的P調(diào)度。然后M0將進入緩存池睡眠。

如下圖

GOMAXPROCS設(shè)置P的數(shù)量,最多有GOMAXPROCS個線程分布在多個CPU上同時運行

在Go中一個goroutine最多占用CPU 10ms,防止其他goroutine被餓死。

具體可以去看另一篇文章

【Golang詳解】go語言調(diào)度機制 搶占式調(diào)度

當創(chuàng)建一個新的G之后優(yōu)先加入本地隊列,如果本地隊列滿了,會將本地隊列的G移動到全局隊列里面,當M執(zhí)行work stealing從其他P偷不到G時,它可以從全局G隊列獲取G。

協(xié)程經(jīng)歷過程

我們創(chuàng)建一個協(xié)程 go func()經(jīng)歷過程如下圖:

說明:

這里有兩個存儲G的隊列,一個是局部調(diào)度器P的本地隊列、一個是全局G隊列。新創(chuàng)建的G會先保存在P的本地隊列中,如果P的本地隊列已經(jīng)滿了就會保存在全局的隊列中;處理器本地隊列是一個使用數(shù)組構(gòu)成的環(huán)形鏈表,它最多可以存儲 256 個待執(zhí)行任務(wù)。

G只能運行在M中,一個M必須持有一個P,M與P是1:1的關(guān)系。M會從P的本地隊列彈出一個可執(zhí)行狀態(tài)的G來執(zhí)行,如果P的本地隊列為空,就會想其他的MP組合偷取一個可執(zhí)行的G來執(zhí)行;

一個M調(diào)度G執(zhí)行的過程是一個循環(huán)機制;會一直從本地隊列或全局隊列中獲取G

上面說到P的個數(shù)默認等于CPU核數(shù),每個M必須持有一個P才可以執(zhí)行G,一般情況下M的個數(shù)會略大于P的個數(shù),這多出來的M將會在G產(chǎn)生系統(tǒng)調(diào)用時發(fā)揮作用。類似線程池,Go也提供一個M的池子,需要時從池子中獲取,用完放回池子,不夠用時就再創(chuàng)建一個。

work-stealing調(diào)度算法:當M執(zhí)行完了當前P的本地隊列隊列里的所有G后,P也不會就這么在那躺尸啥都不干,它會先嘗試從全局隊列隊列尋找G來執(zhí)行,如果全局隊列為空,它會隨機挑選另外一個P,從它的隊列里中拿走一半的G到自己的隊列中執(zhí)行。

如果一切正常,調(diào)度器會以上述的那種方式順暢地運行,但這個世界沒這么美好,總有意外發(fā)生,以下分析goroutine在兩種例外情況下的行為。

Go runtime會在下面的goroutine被阻塞的情況下運行另外一個goroutine:

用戶態(tài)阻塞/喚醒

當goroutine因為channel操作或者network I/O而阻塞時(實際上golang已經(jīng)用netpoller實現(xiàn)了goroutine網(wǎng)絡(luò)I/O阻塞不會導(dǎo)致M被阻塞,僅阻塞G,這里僅僅是舉個栗子),對應(yīng)的G會被放置到某個wait隊列(如channel的waitq),該G的狀態(tài)由_Gruning變?yōu)開Gwaitting,而M會跳過該G嘗試獲取并執(zhí)行下一個G,如果此時沒有可運行的G供M運行,那么M將解綁P,并進入sleep狀態(tài);當阻塞的G被另一端的G2喚醒時(比如channel的可讀/寫通知),G被標記為,嘗試加入G2所在P的runnext(runnext是線程下一個需要執(zhí)行的 Goroutine。), 然后再是P的本地隊列和全局隊列。

系統(tǒng)調(diào)用阻塞

當M執(zhí)行某一個G時候如果發(fā)生了阻塞操作,M會阻塞,如果當前有一些G在執(zhí)行,調(diào)度器會把這個線程M從P中摘除,然后再創(chuàng)建一個新的操作系統(tǒng)的線程(如果有空閑的線程可用就復(fù)用空閑線程)來服務(wù)于這個P。當M系統(tǒng)調(diào)用結(jié)束時候,這個G會嘗試獲取一個空閑的P執(zhí)行,并放入到這個P的本地隊列。如果獲取不到P,那么這個線程M變成休眠狀態(tài), 加入到空閑線程中,然后這個G會被放入全局隊列中。

隊列輪轉(zhuǎn)

可見每個P維護著一個包含G的隊列,不考慮G進入系統(tǒng)調(diào)用或IO操作的情況下,P周期性的將G調(diào)度到M中執(zhí)行,執(zhí)行一小段時間,將上下文保存下來,然后將G放到隊列尾部,然后從隊列中重新取出一個G進行調(diào)度。

除了每個P維護的G隊列以外,還有一個全局的隊列,每個P會周期性地查看全局隊列中是否有G待運行并將其調(diào)度到M中執(zhí)行,全局隊列中G的來源,主要有從系統(tǒng)調(diào)用中恢復(fù)的G。之所以P會周期性地查看全局隊列,也是為了防止全局隊列中的G被餓死。

除了每個P維護的G隊列以外,還有一個全局的隊列,每個P會周期性地查看全局隊列中是否有G待運行并將其調(diào)度到M中執(zhí)行,全局隊列中G的來源,主要有從系統(tǒng)調(diào)用中恢復(fù)的G。之所以P會周期性地查看全局隊列,也是為了防止全局隊列中的G被餓死。

M0

M0是啟動程序后的編號為0的主線程,這個M對應(yīng)的實例會在全局變量rutime.m0中,不需要在heap上分配,M0負責(zé)執(zhí)行初始化操作和啟動第一個G,在之后M0就和其他的M一樣了

G0

G0是每次啟動一個M都會第一個創(chuàng)建的goroutine,G0僅用于負責(zé)調(diào)度G,G0不指向任何可執(zhí)行的函數(shù),每個M都會有一個自己的G0,在調(diào)度或系統(tǒng)調(diào)用時會使用G0的??臻g,全局變量的G0是M0的G0

一個G由于調(diào)度被中斷,此后如何恢復(fù)?

中斷的時候?qū)⒓拇嫫骼锏臈P畔ⅲ4娴阶约旱腉對象里面。當再次輪到自己執(zhí)行時,將自己保存的棧信息復(fù)制到寄存器里面,這樣就接著上次之后運行了。

我這里只是根據(jù)自己的理解進行了簡單的介紹,想要詳細了解有關(guān)GMP的底層原理可以去看Go調(diào)度器 G-P-M 模型的設(shè)計者的文檔或直接看源碼

參考: ()

()

文章標題:go語言heap go語言和php 區(qū)別
瀏覽地址:http://muchs.cn/article26/hgepjg.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供ChatGPT、網(wǎng)站制作、標簽優(yōu)化App開發(fā)、響應(yīng)式網(wǎng)站、服務(wù)器托管

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)

外貿(mào)網(wǎng)站建設(shè)