溫馨提示×

溫馨提示×

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

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Golang在runtime中的知識點有哪些

發(fā)布時間:2021-10-21 09:40:23 來源:億速云 閱讀:108 作者:iii 欄目:web開發(fā)

這篇文章主要講解了“Golang在runtime中的知識點有哪些”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“Golang在runtime中的知識點有哪些”吧!

調度器結構

調度器管理三個在 runtime 中十分重要的類型:G、M和P。哪怕你不寫 scheduler 相關代碼,你也應當要了解這些概念。

G、M 和 P

一個G就是一個 goroutine,在 runtime 中通過類型g來表示。當一個 goroutine  退出時,g對象會被放到一個空閑的g對象池中以用于后續(xù)的 goroutine 的使用(譯者注:減少內存分配開銷)。

一個M就是一個系統(tǒng)的線程,系統(tǒng)線程可以執(zhí)行用戶的 go 代碼、runtime 代碼、系統(tǒng)調用或者空閑等待。在 runtime  中通過類型m來表示。在同一時間,可能有任意數(shù)量的M,因為任意數(shù)量的M可能會阻塞在系統(tǒng)調用中。(譯者注:當一個M執(zhí)行阻塞的系統(tǒng)調用時,會將M和P解綁,并創(chuàng)建出一個新的M來執(zhí)行P上的其它G。)

最后,一個P代表了執(zhí)行用戶 go 代碼所需要的資源,比如調度器狀態(tài)、內存分配器狀態(tài)等。在 runtime  中通過類型p來表示。P的數(shù)量精確地(exactly)等于GOMAXPROCS。一個P可以被理解為是操作系統(tǒng)調度器中的 CPU,p類型可以被理解為是每個 CPU  的狀態(tài)。在這里可以放一些需要高效共享但并不是針對每個P(Per P)或者每個M(Per M)的狀態(tài)(譯者注:意思是,可以放一些以P級別共享的數(shù)據(jù))。

調度器的工作是將一個G(需要執(zhí)行的代碼)、一個M(代碼執(zhí)行的地方)和一個P(代碼執(zhí)行所需要的權限和資源)結合起來。當一個M停止執(zhí)行用戶代碼的時候(比如進入阻塞的系統(tǒng)調用的時候),就需要把它的P歸還到空閑的P池中;為了繼續(xù)執(zhí)行用戶的  go 代碼(比如從阻塞的系統(tǒng)調用退出的時候),就需要從空閑的P池中獲取一個P。

所有的g、m和p對象都是分配在堆上且永不釋放的,所以它們的內存使用是很穩(wěn)定的。得益于此,runtime  可以在調度器實現(xiàn)中避免寫屏障(譯者注:垃圾回收時需要的一種屏障,會帶來一些性能開銷)。

用戶棧和系統(tǒng)棧

每個存活著的(non-dead)G都會有一個相關聯(lián)的用戶棧,用戶的代碼就是在這個用戶棧上執(zhí)行的。用戶棧一開始很小(比如  2K),并且動態(tài)地生長或者收縮。

每一個M都有一個相關聯(lián)的系統(tǒng)棧(也被稱為g0棧,因為這個棧也是通過g實現(xiàn)的);如果是在 Unix 平臺上,還會有一個  signal棧(也被稱為gsignal棧)。系統(tǒng)棧和signal棧不能生長,但是足夠大到運行任何 runtime 和 cgo 的代碼(在純 go 二進制中為  8K,在 cgo 情況下由系統(tǒng)分配)。

runtime  代碼經(jīng)常通過調用systemstack、mcall或者asmcgocall臨時性的切換到系統(tǒng)棧去執(zhí)行一些特殊的任務,比如:不能被搶占的、不應該擴張用戶棧的和會切換用戶  goroutine 的。在系統(tǒng)棧上運行的代碼隱含了不可搶占的含義,同時垃圾回收器不會掃描系統(tǒng)棧。當一個M在系統(tǒng)棧上運行時,當前的用戶棧是沒有被運行的。

getg()和getg().m.curg

如果想要獲取當前用戶的g,需要使用getg().m.curg。

getg()雖然會返回當前的g,但是當正在系統(tǒng)?;蛘遱ignal棧上執(zhí)行的時候,會返回的是當前M的g0或者gsignal,而這很可能不是你想要的。

如果要判斷當前正在系統(tǒng)棧上執(zhí)行還是用戶棧上執(zhí)行,可以使用getg() == getg().m.curg。

錯誤處理和上報

在用戶代碼中,有一些可以被合理地(reasonably)恢復的錯誤可以像往常一樣使用panic,但是有一些情況下,panic可能導致立即的致命的錯誤,比如在系統(tǒng)棧中調用或者當執(zhí)行mallocgc時。

大部分的 runtime  的錯誤是不可恢復的,對于這些不可恢復的錯誤應該使用throw,throw會打印出traceback并立即終止進程。throw應當被傳入一個字符串常量以避免在該情況下還需要為  string 分配內存。根據(jù)約定,更多的信息應當在throw之前使用print或者println打印出來,并且應當以runtime.開頭。

為了進行 runtime 的錯誤調試,有一個很實用的方法是設置GOTRACEBACK=system 或 GOTRACEBACK=crash。

同步

runtime 中有多種同步機制,這些同步機制不僅是語義上不同,和 go 調度器以及操作系統(tǒng)調度器之間的交互也是不一樣的。

最簡單的就是mutex,可以使用lock和unlock來操作。這種方法主要用來短期(長期的話性能差)地保護一些共享的數(shù)據(jù)。在mutex上阻塞會直接阻塞整個M,而不會和  go 的調度器進行交互。因此,在 runtime 中的最底層使用  mutex是安全的,因為它還會阻止相關聯(lián)的G和P被重新調度(M都阻塞了,無法執(zhí)行調度了)。rwmutex也是類似的。

如果是要進行一次性的通知,可以使用note。note提供了notesleep和notewakeup。不像傳統(tǒng)的 UNIX  的sleep/wakeup,note是無競爭的(race-free),所以如果notewakeup已經(jīng)發(fā)生了,那么notesleep將會立即返回。note可以在使用后通過noteclear來重置,但是要注意noteclear和notesleep、notewakeup不能發(fā)生競爭。類似mutex,阻塞在note上會阻塞整個M。然而,note提供了不同的方式來調用sleep:notesleep會阻止相關聯(lián)的G和P被重新調度;notetsleepg的表現(xiàn)卻像一個阻塞的系統(tǒng)調用一樣,允許P被重用去運行另一個G。盡管如此,這仍然比直接阻塞一個G要低效,因為這需要消耗一個M。

如果需要直接和 go 調度器交互,可以使用gopark和goready。gopark掛起當前的  goroutine——把它變成waiting狀態(tài),并從調度器的運行隊列中移除——然后調度另一個 goroutine  到當前的M或者P。goready將一個被掛起的 goroutine 恢復到runnable狀態(tài)并將它放到運行隊列中。

總結起來如下表:

Golang在runtime中的知識點有哪些

原子性

runtime  使用runtime/internal/atomic中自有的一些原子操作。這個和sync/atomic是對應的,除了方法名由于歷史原因有一些區(qū)別,并且有一些額外的  runtime 需要的方法。

總的來說,我們對于 runtime 中 atomic  的使用非常謹慎,并且盡可能避免不需要的原子操作。如果對于一個變量的訪問已經(jīng)被另一種同步機制所保護,那么這個已經(jīng)被保護的訪問一般就不需要是原子的。這么做主要有以下原因:

  • 合理地使用非原子和原子操作使得代碼更加清晰可讀,對于一個變量的原子操作意味著在另一處可能會有并發(fā)的對于這個變量的操作。

  • 非原子的操作允許自動的競爭檢測。runtime  本身目前并沒有一個競爭檢測器,但是未來可能會有。原子操作會使得競爭檢測器忽視掉這個檢測,但是非原子的操作可以通過競爭檢測器來驗證你的假設(是否會發(fā)生競爭)。

  • 非原子的操作可以提高性能。

當然,所有對于一個共享變量的非原子的操作都應當在文檔中注明該操作是如何被保護的。

有一些比較普遍的將原子操作和非原子操作混合在一起的場景有:

  • 大部分操作都是讀,且寫操作被鎖保護的變量。在鎖保護的范圍內,讀操作沒必要是原子的,但是寫操作必須是原子的。在鎖保護的范圍外,讀操作必須是原子的。

  • 僅僅在 STW 期間發(fā)生的讀操作,且 STW 期間不會有寫操作。那么這個時候,讀操作不需要是原子的。

話雖如此,Go Memory Model給出的建議仍然成立Don't be [too] clever。runtime  的性能固然重要,但是魯棒性(robustness)卻更加重要。

堆外內存(Unmanaged memory)

一般情況下,runtime 會嘗試使用普通的方法來申請內存(堆上內存,gc 管理的),然而在某些情況 runtime 必須申請一些不被 gc  所管理的堆外內存(unmanaged  memory)。這是很必要的,因為有可能該片內存就是內存管理器自身,或者說調用者沒有一個P(譯者注:比如在調度器初始化之前,是不存在P的)。

有三種方式可以申請堆外內存:

  • sysAlloc直接從操作系統(tǒng)獲取內存,申請的內存必須是系統(tǒng)頁表長度的整數(shù)倍??梢酝ㄟ^sysFree來釋放。

  • persistentalloc將多個小的內存申請合并在一起為一個大的sysAlloc以避免內存碎片(fragmentation)。然而,顧名思義,通過persistentalloc申請的內存是無法被釋放的。

  • fixalloc是一個SLAB風格的內存分配器,分配固定大小的內存。通過fixalloc分配的對象可以被釋放,但是內存僅可以被相同的fixalloc池所重用。所以fixalloc適合用于相同類型的對象。

普遍來說,使用以上三種方法分配內存的類型都應該被標記為//go:notinheap(見后文)。

在堆外內存所分配的對象不應該包含堆上的指針對象,除非同時遵守了以下的規(guī)則:

  1. 鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術社區(qū)

  2. 所有在堆外內存指向堆上的指針都必須是垃圾回收的根(garbage collection  roots)。也就是說,所有指針必須可以通過一個全局變量所訪問到,或者顯式地使用runtime.markroot來標記。

  3. 如果內存被重用了,堆上的指針在被標記為 GC 根并且對 GC 可見前必須 以 0 初始化(zero-initialized,見后文)。不然的話,GC  可能會觀察到過期的(stale)堆指針??梢詤⒁娤挛腪ero-initialization versus zeroing.

Zero-initialization versus zeroing

在 runtime 中有兩種類型的零初始化,取決于內存是否已經(jīng)初始化為了一個類型安全的狀態(tài)。

如果內存不在一個類型安全的狀態(tài),意思是可能由于剛被分配,并且第一次初始化使用,會含有一些垃圾值(譯者注:這個概念在日常的 Go 代碼中是遇不到的,如果學過  C  語言的同學應該能理解什么意思),那么這片內存必須使用memclrNoHeapPointers進行zero-initialized或者無指針的寫。這不會觸發(fā)寫屏障(譯者注:寫屏障是  GC 中的一個概念)。

內存可以通過typedmemclr或者memclrHasPointers來寫入零值,設置為類型安全的狀態(tài)。這會觸發(fā)寫屏障。

Runtime-only 編譯指令(compiler directives)

除了go doc compile中注明的//go:編譯指令外,編譯器在 runtime 包中支持了額外的一些指令。

go:systemstack

go:systemstack表明一個函數(shù)必須在系統(tǒng)棧上運行,這個會通過一個特殊的函數(shù)前引(prologue)動態(tài)地驗證。

go:nowritebarrier

go:nowritebarrier告知編譯器如果以下函數(shù)包含了寫屏障,觸發(fā)一個錯誤(這不會阻止寫屏障的生成,只是單純一個假設)。

一般情況下你應該使用go:nowritebarrierrec。go:nowritebarrier當且僅當“最好不要”寫屏障,但是非正確性必須的情況下使用。

go:nowritebarrierrec 與 go:yeswritebarrierrec

go:nowritebarrierrec告知編譯器如果以下函數(shù)以及它調用的函數(shù)(遞歸下去),直到一個go:yeswritebarrierrec為止,包含了一個寫屏障的話,觸發(fā)一個錯誤。

邏輯上,編譯器會在生成的調用圖上從每個go:nowritebarrierrec函數(shù)出發(fā),直到遇到了go:yeswritebarrierrec的函數(shù)(或者結束)為止。如果其中遇到一個函數(shù)包含寫屏障,那么就會產生一個錯誤。

go:nowritebarrierrec主要用來實現(xiàn)寫屏障自身,用來避免死循環(huán)。

這兩種編譯指令都在調度器中所使用。寫屏障需要一個活躍的P(getg().m.p !=  nil),然而調度器相關代碼有可能在沒有一個活躍的P的情況下運行。在這種情況下,go:nowritebarrierrec會用在一些釋放P或者沒有P的函數(shù)上運行,go:yeswritebarrierrec會用在重新獲取到了P的代碼上。因為這些都是函數(shù)級別的注釋,所以釋放P和獲取P的代碼必須被拆分成兩個函數(shù)。

go:notinheap

go:notinheap適用于類型聲明,表明了一個類型必須不被分配在 GC  堆上。特別的,指向該類型的指針總是應當在runtime.inheap判斷中失敗。這個類型可能被用于全局變量、棧上變量,或者堆外內存上的對象(比如通過sysAlloc、persistentalloc、fixalloc或者其它手動管理的span進行分配)。特別的:

  1. 鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術社區(qū)

  2. new(T)、make([]T)、append([]T, ...)和隱式的對于T的堆上分配是不允許的(盡管隱式的分配在 runtime  中是從來不被允許的)。

  3. 一個指向普通類型的指針(除了unsafe.Pointer)不能被轉換成一個指向go:notinheap類型的指針,就算它們有相同的底層類型(underlying  type)。

  4. 任何一個包含了go:notinheap類型的類型自身也是go:notinheap的。如果結構體和數(shù)組包含go:notinheap的元素,那么它們自身也是go:notinheap類型。map  和 channel  不允許有go:notinheap類型。為了使得事情更加清晰,任何隱式的go:notinheap類型都應該顯式地標明go:notinheap。

  5. 指向go:notinheap類型的指針的寫屏障可以被忽略。

最后一點是go:notinheap類型真正的好處。runtime  在底層結構中使用這個來避免調度器和內存分配器的內存屏障以避免非法檢查或者單純提高性能。這種方法是適度的安全(reasonably safe)的并且不會使得  runtime 的可讀性降低。

感謝各位的閱讀,以上就是“Golang在runtime中的知識點有哪些”的內容了,經(jīng)過本文的學習后,相信大家對Golang在runtime中的知識點有哪些這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!

向AI問一下細節(jié)

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

AI