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

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

評價go語言底層原理剖析 go語言基于什么

圖解Go中select語句的底層原理

Go 的select語句是一種僅能用于channl發(fā)送和接收消息的專用語句,此語句運行期間是阻塞的;當select中沒有case語句的時候,會阻塞當前的groutine。所以,有人也會說select是用來阻塞監(jiān)聽goroutine的。

創(chuàng)新互聯建站主營西山網站建設的網絡公司,主營網站建設方案,重慶APP軟件開發(fā),西山h5微信平臺小程序開發(fā)搭建,西山網站營銷推廣歡迎西山等地區(qū)企業(yè)咨詢

還有人說:select是Golang在語言層面提供的I/O多路復用的機制,其專門用來檢測多個channel是否準備完畢:可讀或可寫。

以上說法都正確。

我們來回顧一下是什么是 I/O多路復用 。

每來一個進程,都會建立連接,然后阻塞,直到接收到數據返回響應。

普通這種方式的缺點其實很明顯:系統(tǒng)需要創(chuàng)建和維護額外的線程或進程。因為大多數時候,大部分阻塞的線程或進程是處于等待狀態(tài),只有少部分會接收并處理響應,而其余的都在等待。系統(tǒng)為此還需要多做很多額外的線程或者進程的管理工作。

為了解決圖中這些多余的線程或者進程,于是有了"I/O多路復用"

每個線程或者進程都先到圖中”裝置“中注冊,然后阻塞,然后只有一個線程在”運輸“,當注冊的線程或者進程準備好數據后,”裝置“會根據注冊的信息得到相應的數據。從始至終kernel只會使用圖中這個黃黃的線程,無需再對額外的線程或者進程進行管理,提升了效率。

select的實現經歷了多個版本的修改,當前版本為:1.11

select這個語句底層實現實際上主要由兩部分組成: case語句 和 執(zhí)行函數 。

源碼地址為:/go/src/runtime/select.go

每個case語句,單獨抽象出以下結構體:

結構體可以用下圖表示:

然后執(zhí)行select語句實際上就是調用 func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) 函數。

func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) 函數參數:

selectgo 返回所選scase的索引(該索引與其各自的select {recv,send,default}調用的序號位置相匹配)。此外,如果選擇的scase是接收操作(recv),則返回是否接收到值。

誰負責調用 func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) 函數呢?

在 /reflect/value.go 中有個 func rselect([]runtimeSelect) (chosen int, recvOK bool) 函數,此函數的實現在 /runtime/select.go 文件中的 func reflect_rselect(cases []runtimeSelect) (int, bool) 函數中:

那誰調用的 func rselect([]runtimeSelect) (chosen int, recvOK bool) 呢?

在 /refect/value.go 中,有一個 func Select(cases []SelectCase) (chosen int, recv Value, recvOK bool) 的函數,其調用了 rselect 函數,并將最終Go中select語句的返回值的返回。

以上這三個函數的調用棧按順序如下:

這仨函數中無論是返回值還是參數都大同小異,可以簡單粗暴的認為:函數參數傳入的是case語句,返回值返回被選中的case語句。

那誰調用了 func Select(cases []SelectCase) (chosen int, recv Value, recvOK bool) 呢?

可以簡單的認為是系統(tǒng)了。

來個簡單的圖:

前兩個函數 Select 和 rselect 都是做了簡單的初始化參數,調用下一個函數的操作。select真正的核心功能,是在最后一個函數 func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) 中實現的。

打亂傳入的case結構體順序

鎖住其中的所有的channel

遍歷所有的channel,查看其是否可讀或者可寫

如果其中的channel可讀或者可寫,則解鎖所有channel,并返回對應的channel數據

假如沒有channel可讀或者可寫,但是有default語句,則同上:返回default語句對應的scase并解鎖所有的channel。

假如既沒有channel可讀或者可寫,也沒有default語句,則將當前運行的groutine阻塞,并加入到當前所有channel的等待隊列中去。

然后解鎖所有channel,等待被喚醒。

此時如果有個channel可讀或者可寫ready了,則喚醒,并再次加鎖所有channel,

遍歷所有channel找到那個對應的channel和G,喚醒G,并將沒有成功的G從所有channel的等待隊列中移除。

如果對應的scase值不為空,則返回需要的值,并解鎖所有channel

如果對應的scase為空,則循環(huán)此過程。

在想想select和channel做了什么事兒,我覺得和多路復用是一回事兒

goland map底層原理

map 是Go語言中基礎的數據結構,在日常的使用中經常被用到。但是它底層是如何實現的呢?

總體來說golang的map是hashmap,是使用數組+鏈表的形式實現的,使用拉鏈法消除hash沖突。

golang的map由兩種重要的結構,hmap和bmap(下文中都有解釋),主要就是hmap中包含一個指向bmap數組的指針,key經過hash函數之后得到一個數,這個數低位用于選擇bmap(當作bmap數組指針的下表),高位用于放在bmap的[8]uint8數組中,用于快速試錯。然后一個bmap可以指向下一個bmap(拉鏈)。

Golang中map的底層實現是一個散列表,因此實現map的過程實際上就是實現散表的過程。在這個散列表中,主要出現的結構體有兩個,一個叫 hmap (a header for a go map),一個叫 bmap (a bucket for a Go map,通常叫其bucket)。這兩種結構的樣子分別如下所示:

hmap :

圖中有很多字段,但是便于理解map的架構,你只需要關心的只有一個,就是標紅的字段: buckets數組 。Golang的map中用于存儲的結構是bucket數組。而bucket(即bmap)的結構是怎樣的呢?

bucket :

相比于hmap,bucket的結構顯得簡單一些,標紅的字段依然是“核心”,我們使用的map中的key和value就存儲在這里?!案呶还V怠睌到M記錄的是當前bucket中key相關的“索引”,稍后會詳細敘述。還有一個字段是一個指向擴容后的bucket的指針,使得bucket會形成一個鏈表結構。例如下圖:

由此看出hmap和bucket的關系是這樣的:

而bucket又是一個鏈表,所以,整體的結構應該是這樣的:

哈希表的特點是會有一個哈希函數,對你傳來的key進行哈希運算,得到唯一的值,一般情況下都是一個數值。Golang的map中也有這么一個哈希函數,也會算出唯一的值,對于這個值的使用,Golang也是很有意思。

Golang把求得的值按照用途一分為二:高位和低位。

如圖所示,藍色為高位,紅色為低位。 然后低位用于尋找當前key屬于hmap中的哪個bucket,而高位用于尋找bucket中的哪個key。上文中提到:bucket中有個屬性字段是“高位哈希值”數組,這里存的就是藍色的高位值,用來聲明當前bucket中有哪些“key”,便于搜索查找。 需要特別指出的一點是:我們map中的key/value值都是存到同一個數組中的。數組中的順序是這樣的:

并不是key0/value0/key1/value1的形式,這樣做的好處是:在key和value的長度不同的時候,可 以消除padding(內存對齊)帶來的空間浪費 。

現在,我們可以得到Go語言map的整個的結構圖了:(hash結果的低位用于選擇把KV放在bmap數組中的哪一個bmap中,高位用于key的快速預覽,用于快速試錯)

map的擴容

當以上的哈希表增長的時候,Go語言會將bucket數組的數量擴充一倍,產生一個新的bucket數組,并將舊數組的數據遷移至新數組。

加載因子

判斷擴充的條件,就是哈希表中的加載因子(即loadFactor)。

加載因子是一個閾值,一般表示為:散列包含的元素數 除以 位置總數。是一種“產生沖突機會”和“空間使用”的平衡與折中:加載因子越小,說明空間空置率高,空間使用率小,但是加載因子越大,說明空間利用率上去了,但是“產生沖突機會”高了。

每種哈希表的都會有一個加載因子,數值超過加載因子就會為哈希表擴容。

Golang的map的加載因子的公式是:map長度 / 2^B(這是代表bmap數組的長度,B是取的低位的位數)閾值是6.5。其中B可以理解為已擴容的次數。

當Go的map長度增長到大于加載因子所需的map長度時,Go語言就會將產生一個新的bucket數組,然后把舊的bucket數組移到一個屬性字段oldbucket中。注意:并不是立刻把舊的數組中的元素轉義到新的bucket當中,而是,只有當訪問到具體的某個bucket的時候,會把bucket中的數據轉移到新的bucket中。

如下圖所示:當擴容的時候,Go的map結構體中,會保存舊的數據,和新生成的數組

上面部分代表舊的有數據的bucket,下面部分代表新生成的新的bucket。藍色代表存有數據的bucket,橘黃色代表空的bucket。

擴容時map并不會立即把新數據做遷移,而是當訪問原來舊bucket的數據的時候,才把舊數據做遷移,如下圖:

注意:這里并不會直接刪除舊的bucket,而是把原來的引用去掉,利用GC清除內存。

map中數據的刪除

如果理解了map的整體結構,那么查找、更新、刪除的基本步驟應該都很清楚了。這里不再贅述。

值得注意的是,找到了map中的數據之后,針對key和value分別做如下操作:

1

2

3

4

1、如果``key``是一個指針類型的,則直接將其置為空,等待GC清除;

2、如果是值類型的,則清除相關內存。

3、同理,對``value``做相同的操作。

4、最后把key對應的高位值對應的數組index置為空。

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

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

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

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

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)調用結束后,根據M0是否能獲取到P,將會將G0做不同的處理:

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

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

如下圖

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

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

具體可以去看另一篇文章

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

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

協程經歷過程

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

說明:

這里有兩個存儲G的隊列,一個是局部調度器P的本地隊列、一個是全局G隊列。新創(chuàng)建的G會先保存在P的本地隊列中,如果P的本地隊列已經滿了就會保存在全局的隊列中;處理器本地隊列是一個使用數組構成的環(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的個數默認等于CPU核數,每個M必須持有一個P才可以執(zhí)行G,一般情況下M的個數會略大于P的個數,這多出來的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實現了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í)行的函數,每個M都會有一個自己的G0,在調度或系統(tǒng)調用時會使用G0的??臻g,全局變量的G0是M0的G0

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

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

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

參考: ()

()


本文標題:評價go語言底層原理剖析 go語言基于什么
網頁網址:http://weahome.cn/article/dodddij.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部