真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網站制作重慶分公司

go語言分析 go語言分析tcpdump文件

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

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

多端合一響應式網站:PC+平板+手機,同一后臺修改數(shù)據(jù)多端同步更新提交您的需求,獲取網站建設與營銷策劃方案報價,我們會在1小時內與您聯(lián)系!

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

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

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

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

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

模型圖:

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

1)work stealing機制

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

2)hand off機制

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

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

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

如下圖

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

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

具體可以去看另一篇文章

【Golang詳解】go語言調度機制 搶占式調度

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

協(xié)程經歷過程

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

說明:

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

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

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

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

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

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

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

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

當goroutine因為channel操作或者network I/O而阻塞時(實際上golang已經用netpoller實現(xiàn)了goroutine網絡I/O阻塞不會導致M被阻塞,僅阻塞G,這里僅僅是舉個栗子),對應的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)調用阻塞

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

隊列輪轉

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

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

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

M0

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

G0

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

一個G由于調度被中斷,此后如何恢復?

中斷的時候將寄存器里的棧信息,保存到自己的G對象里面。當再次輪到自己執(zhí)行時,將自己保存的棧信息復制到寄存器里面,這樣就接著上次之后運行了。

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

參考: ()

()

go分析要多久

Go語言的分析取決于多方面的因素,如代碼量、理解深度、復雜性等。如果是一般的小規(guī)模項目,比如幾百行代碼,那么通常需要幾個小時來分析。但對于大規(guī)模項目,比如幾萬行以上,則需要更多的時間來分析,可能會有幾天或更長的時間。同時,如果開發(fā)者對代碼深入理解的程度越高,分析的時間也會更長。

go語言語法(基礎語法篇)

import "workname/packetfolder"

導入多個包

方法調用 包名.函數(shù)//不是函數(shù)或結構體所處文件或文件夾名

packagename.Func()

前面加個點表示省略調用,那么調用該模塊里面的函數(shù),可以不用寫模塊名稱了:

當導入一個包時,該包下的文件里所有init()函數(shù)都會被執(zhí)行,然而,有些時候我們并不需要把整個包都導入進來,僅僅是是希望它執(zhí)行init()函數(shù)而已。下劃線的作用僅僅是為了調用init()函數(shù),所以無法通過包名來調用包中的其他函數(shù)

import _ package

變量聲明必須要使用否則會報錯。

全局變量運行聲明但不使用。

func 函數(shù)名 (參數(shù)1,參數(shù)2,...) (返回值a 類型a, 返回值b 類型b,...)

func 函數(shù)名 (參數(shù)1,參數(shù)2,...) (返回值類型1, 返回值類型2,...)

func (this *結構體名) 函數(shù)名(參數(shù) string) (返回值類型1, 返回值類型2){}

使用大小來區(qū)分函數(shù)可見性

大寫是public類型

小寫是private類型

func prifunc int{}

func pubfunc int{}

聲明靜態(tài)變量

const value int

定義變量

var value int

聲明一般類型、接口和結構體

聲明函數(shù)

func function () int{}

go里面所有的空值對應如下

通道類型

內建函數(shù) new 用來分配內存,它的第一個參數(shù)是一個類型,不是一個值,它的返回值是一個指向新分配類型零值的指針

func new(Type) *Type

[這位博主有非常詳細的分析]

Go 語言支持并發(fā),我們只需要通過 go 關鍵字來開啟 goroutine 即可。

goroutine 是輕量級線程,goroutine 的調度是由 Golang 運行時進行管理的。

同一個程序中的所有 goroutine 共享同一個地址空間。

語法格式如下:

通道(channel)是用來傳遞數(shù)據(jù)的一個數(shù)據(jù)結構。

通道的聲明

通道可用于兩個 goroutine 之間通過傳遞一個指定類型的值來同步運行和通訊。操作符 - 用于指定通道的方向,發(fā)送或接收。如果未指定方向,則為雙向通道。

[這里有比較詳細的用例]

go里面的空接口可以指代任何類型(無論是變量還是函數(shù))

聲明空接口

go里面的的強制類型轉換語法為:

int(data)

如果是接口類型的強制轉成其他類型的語法為:

go里面的強制轉換是將值復制過去,所以在數(shù)據(jù)量的時候有比較高的運行代價

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

多級間接賦值容易導致逃逸 。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Benchmark 和 pprof 給出的結果:

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

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

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


分享名稱:go語言分析 go語言分析tcpdump文件
本文路徑:http://weahome.cn/article/ddeghho.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部