1. 程式人生 > >Go語言下的執行緒模型

Go語言下的執行緒模型

閱讀Go併發程式設計對go語言執行緒模型的筆記,解釋的非常到,好記性不如爛筆頭,忘記的時候回來翻一番,在此做下筆記。

Go語言的執行緒實現模型,又3個必知的核心元素,他們支撐起了這個執行緒實現模型的主要框架: 1>M:Machine的縮寫。一個M代表一個核心執行緒。 2>P:Procecssor的縮寫。一個P代表了M所在的上下文環境。 3>G:Goroutine的縮寫。一個G代表了對一段需要被併發執行的Go語言程式碼的封裝。

簡單的來說,一個G的執行檔案需要M和P的支援。一個M在與一個P關聯形成一個有效的G執行環境(核心執行緒+上下文環境)。 每個P都會包含一個可執行的G的佇列(runq)。該佇列的G會被依次傳給與本地P關聯的M並獲得執行時機。在這裡, 我們把運行當前程式的那個M稱為當前M,而把與當前M關聯的那個P稱為本地P。

M(Machine)與KSE(Kernel Schedule Entity)之間總一對一的。一個M能且僅代表一個核心執行緒。Go語言的執行 時系統(runtime system)用它來代表一個核心排程系統。

1.M(Machine)

一個M代表了一個核心執行緒。大多數情況下,建立一個M的原因都是由於沒有足夠的M來關聯P(Process) 並執行其中的可執行的G。不過,在執行時系統執行系統監控或垃圾回收等任務的時候也會 導致新的M的建立。M(Machine)的資料結構包括(curg p mstartfn nextp)。 image.png

M(Machine)結構中的欄位眾多。我們在這裡只是挑選了對於我們的初步認識M(Machine)最重要的4個欄位。其中欄位 curg會存放當前M正在執行的那個G(goroutine)的指標,欄位p會指向與當前M相關聯的那個P,而欄位mstartfm則代表 我們馬上會講到的M(Machine)的起始函式。在M被排程的過程中,這三個欄位最能體現他的即使情況。而另外的欄位nextp則 會被用於暫存與當前M(Machine)又潛在關係的P。我們可以把排程器將某個P(Process)賦值給某個M的nextp欄位的操作稱為 M和P的預聯。在有些時候,執行時系統給會把剛剛被重啟新啟用的M(Machine)和它預聯的那個P關聯在一起,這就是nextp欄位的所起到的作用。

M被建立之初會被加入全域性的M(Machine)列表(runtie.allm)中。緊接著,它的起始函式和準備關聯的P(Process)(大多數 情況下導致次M(Machine)建立操作的那個P(Process))會被設定。最後,執行時系統會為它專門建立一個新的核心執行緒並與之 關聯。這樣,這個新的M(Machine)就為執行G(Goroutine)做好了準備。而這裡的全域性M(Machine)列表其實並沒有什麼特殊的意義。 執行時系統在需要的時候會通過它獲取所有M的資訊。同時它也防止M被當作垃圾回收。

在新的M被建立完成之後的會先進行一番初始化工作。其中包括了對自身所持的棧空間以及訊號處理方面的初始化。 在這些初始化工作都完成之後。該M將會被執行(如果存在的話)。注意,如果在這個起始函式代表的是系統監控的任務 的話,那麼該M會一直在那裡執行而不會繼續後面的流程。否則,在初始函式被執行完畢後。當前M將會與那個準備與 它關聯的P完成關聯。至此,一個併發執行環境才真正的形成。在這之後,M開始尋找可執行的G並執行它,這一過程 可以被看做是排程的一部分。

執行時系統所管轄的M(或者說runtime.allm中的M)有時候會被停止,比如在執行時系統準備開始執行垃圾回收任務時候。 執行時系統停止在M的時候,會對它的屬性進行必要的重置之後,把它放進排程器的空閒M列表(runtime.sched.midle)。 因為在需要一個未被使用的M的時候,執行時系統會嘗試從該列表中。

注意,M本身是無狀態的。M是否空閒僅僅以為它是否存在於排程器的空閒M列表中為依據。雖然執行時系統可以通過M列表 獲取所有的M,但是卻無法得知它們的狀態(因為它們沒有狀態)。

單個Go程式所使用的M最大資料是可以被設定的。在我們使用命令執行Go程式的時候,一個載入程式先會被啟動。 這個載入程式先會被啟動,這個初始值是1w。也就是說,一個Go程式最多可以使用1w個M。 這就以為著。在最理想的情況下,同時可以有1w個核心執行緒同時被執行。請注意,這裡說的是最理想的i情況下的。 由於作業系統的核心對程序的虛擬記憶體的佈局的控制以及大小的限制,如此量級的執行緒很難共存。從這個角度看。 Go語言本身對執行緒的執行緒數量幾乎可以被忽略。

出了上述設定外,我們也可以在Go程式中對該限制進行設定。為了達到此目的,我們需要呼叫標準庫的程式碼包runtime/debug包 中的SetMaxThreads函式並且對提供新的M最大數量。runtime/debug.SetMaxThreads函式在執行後,會把舊的M最大數量作為結果 值返回。非常重要的一點是,如果我嫩在呼叫runtime/debug.seMaxThreads函式時給定的新值比當時M的實際數量還要小的話, 執行時系統就會發起一個執行時恐慌。所以,我們要小心使用這個函式。請記住,如果我們真的需要設定M的最大數量。 那麼也早呼叫runtime/debug.SetMaxThreads函式就也好,對於它的設定值,我們也要仔細斟酌。

2.P(Process)

P(Process)是使G能夠在M中執行的關鍵。Golang的執行時系統會實時地讓P與不同的M建立或斷開關聯,以使P中的那些可執行的 G能夠在需要的時候及時獲得執行時機。這與作業系統核心在CPU之上切換不同的程序或者執行緒類似。

通過呼叫函式runtime.GOMAXPROCS,我們可以改變單個Go程式可以間接擁有的P的最大數量。初除此自外,我們還可以在執行Go程式 之前設定環境變數GOMAXPROCS的值對Go程式的可以用的P最大的數量做出預先設定。P的最大數量相當於是對可以被併發執行的使用者 級別的G的數量做出限制。我們已經知道,每個P都需要關聯一個M(Machine)才能使其中的可執行的G得到執行。但是這卻不意味著 環境變數GOMAXPROCS的值會被限制住M的總數量。當M因系統呼叫的進行而被阻塞(更切確的說,是它執行的G進入了系統的呼叫)的 時候,執行時系統會將該M和與之關聯的P分離出來。這時,如果這個P的可執行G佇列中還未被執行的G,那麼執行時系統 就會找到一個空閒M,或創建出一個新的M,並與該P關聯以滿足這些G執行需要。如果我們在Go程式中建立大部分Goroutine中 都包含了很多需要的間接地進行各種系統呼叫(比如各種I/O操作)程式碼的話,那麼即使環境變數GOMAXPROCS的值被設定未1,也 很可能被建立很多個M被創建出來。所以,實際的M總數量很可能比環境變數GOMAXPROCS所指代的數量多。由此可見,Go程式 真正使用的核心執行緒的數量並不會因此而受到限制。

在Go程式開始被執行的時候,我們在前面提到的載入程式也會對P的最大數量進行設定。P的最大數量的預設值是1。因此。 在預設的情況下,無論我們在程式中用go語句啟用多個Goroutine。它們都只會被塞入同一個P的可執行G的佇列中,當 環境變數GOMAXPROCS的值的有效就會被這個硬性限制取代,也就是說,最終的P最大數量值絕對不會比載入程式中的這個硬性 上線值打。該硬性上限值是2的8次方。即256.這個硬性上限值為256的原因是Go語言目前還不能保證在數量比256更多的P同時存在的 情況下Go程式仍能保持高效。也就是說,這個硬行上線並不是永久的,它在以後可能會被改變 [https://stackoverflow.com/questions/40943065/golang-why-runtime-gomaxprocs-is-limited-to-256]現在是1024了。

注意,雖然我們可以在程式中隨意地呼叫runtime.GOMAXPROCS函式,但是它的執行會暫時使所有的P都相繼進入停止狀態並試圖 阻止任何使用者級別的G的執行。只有在新的P最大數量被設定完成後,執行時系統才會開始陸續恢復它們。對於程式的效能是 非常大的損耗。所以,我們只好在Go程式的main函式的開始處呼叫runtime.GOMAXPROCS函式。當然,在Go程式中不對它進行 呼叫而只預先設定環境變數GOMAXPROCS是最好不過的了

在確定P的最大數量之後,執行時系統會根據這個數值初始化全域性的P列表(runtime.allp)與全域性M列表類似,該列表包含了當前 執行時的系統建立的所有P。隨後,執行時系統會把排程器的可執行G佇列(runtime.sched.runq)中的所有G均勻的放入到全域性 列表中。至此,執行時系統需要用到的所有P都已就緒

與空閒M列表類似,所執行時系統中也存在一個排程器的空閒P列表(runtime.sched.pidle)。當一個P不再與任何M關聯的時候, 執行時系統就會把它放入到該列表,當前執行時系統需要一個空閒的P關聯某個M的話,會從次列表取一個出來,由此我們也可知道 空閒P列表的准入條件,注意,即使P進入到了空閒P列表中,它的執行G列表也不一定是空的,兩者之間沒有必然的聯絡。

與M不同,P本身是有狀態的,一個P可能具有的狀態如下: 1>Pidle: 此狀態表明當前P未與任何M存在關聯。 2>Prunning:此狀態表明當前P與某個M關聯。 3>Psyscall:此狀態表明當前P中的被執行的那個G正在進行系統呼叫。 4>Pgcstop:此狀態表明執行系統正在驚醒垃圾回收,在執行時系統驚醒垃圾回收的時候,會試圖把全域性列表中的都置於此狀態。 5>Pdead:此狀態表明當前P已經不會再被呼叫。當我們Go程式執行的過程中通過呼叫。

image.png

runtime.GOMAXPROCS函式減少P最大數量的時候,其餘的P就會被執行時系統置於此狀態。P的初始狀態是Pgcstop, 雖然執行時系統並不會再這時進行垃圾回收。不過,P處於這一初始狀態的時間會非常短暫。緊接著的初始化和填充P中的可 執行G佇列之後,執行時系統會被其狀態設定未Pidle並放入到排程器的空閒列表中。此空閒P列表中的所有P都有排程器根據實際 情況經進行取用。

3.G(Goroutine)

一個G就相當於一個Goroutine(或稱Go程),也與我們使用go語句欲併發執行的一個匿名或命名的函式相對應。我們 作為程式設計人員只使用go語句向Go語言的執行時系統告知了(或提交了)一個個併發任務,而Go語言的執行時系統則會 按照我們的要求併發地執行完成這一任務。

Go語言的編譯器會把我們編寫的go語句(go 關鍵字和其後的函式統稱)變成對一個執行時系統中的函式呼叫,並把go 語句中的那個函式以及其引數都作為引數傳遞給這個執行時系統中的函式。這也是我們應該瞭解的第一件與go語句相關 的事。其實它並不神奇,只是代表了我們向執行時系統遞交了一個任務而已。

執行時系統在接到這樣一個呼叫之後,會先檢查一下go函式及其引數的合法性,緊接著會試圖從本地P的自由G列表和排程器 的自由G列表獲取可用的G。如果沒有獲取到則只好新建一個G了。與M和P相同,執行時系統也持有一個G的全域性列表(runtime.allg)。 新建立的G會在第一時間被加入該列表中。類似地,該列表的主要作用也就是集中存放當前執行時系統中的所有G指標。無論 將會封裝當前的這個go函式的G是否是最新的,執行時系統都會對它進行一次初始化。其中包裹了關聯的go函式以及設定G的 狀態和ID等步驟。在初始化完成後,這個G會被放入到本地P的可執行G佇列中。如果實際成熟,排程會立即進行以使這個G儘快 執行。不過為了及時執行各個可執行的忙碌著。

每個G都會由執行時系統根絕其實際狀態情況設定不同的狀態,其可能的狀態如下。 1>Gidle: 在當前G被建立但還沒有完全未被初始化的時候會處於此狀態。 2>Grunnable:表示當前G是可執行時的,並且正在等待被與執行。 3>Grunning:表示當前G正在被執行。 4>Gsyscall:表示當前G正在進行系統呼叫。 5>Gwaiting:表示當前G正在因某個原因而等待。 6>Gdead:表示當前G已經被執行完成

在執行時系統想用一個G封我們通過go語句遞交的go函式的時候,會對這個G進行初始化。其中的一步就是初始化這個G的 狀態,而這個狀態總會是Grunnable。也就是說,一個G真正的開始被使用是在其狀態被設定Grunnable之後。

image.png

一個G在被執行的過程中,時候會等待某個事件以及會等待什麼樣的事件,完全由其封裝的go函式決定的。例如, 如果這個函式中包含了對通道型別值的操作,那麼在執行到對應的程式碼的時候這個G就有可能進入Gwaiting狀態。 這可能在等待從通道型別值中接受值,也可能是在等待向通道型別傳送值。又例如,設計網路I/O的時候也會導致 相應的G進入Gwaiting狀態。此外,操作定時器(time.Timer)和呼叫time.Sleep函式同樣會造成相應的G的等待。在事件到來 之後,G會被"喚醒"並被轉移到Grunnable狀態。待時機來時,它會在此執行。

G在退出系統呼叫的時候的狀態轉換要比上述情況發雜一些,執行時系統會先嚐試直接執行這個G,僅當無法直接執行的時候,才 會把它轉換成Grunnable狀態並放入到排程器放入自由G列表中,顯然,對這樣的一個G來說,在其退出系統之時就被立即繼續執行 是再好不過的了。執行時系統當然會為此做出一些努力,不過,即使努力失敗了,該G也還是在實時的排程過程中被發現並執行。

最後,值得一提的是,進入死亡狀態(Gdead)的G是可以被重新初始化並使用的。相比之下,P在進入狀態(Pdead)之後則 只能面臨銷燬的結局。由此可以說明Gdead狀態與Pdead狀態所表達的含義是截然不同的。初一Gdead狀態的G會被放入本地P或 排程器的自由G列表,這為它們的重要條件。

至此,我們瞭解到一個G在執行時系統中的流轉方式和時機,著也展現了一條go語句的背後所蘊含的玄機。

核心元素容器

image.png

在這些容器中,全域性的那個3個列表存在的主要目的都分別是為了統計執行時系統中的所有M,P或G。 相比之下。最應該值得我們關注的是那些非全域性的容器,尤其是與G相關的那4個容器。

與G有關的非全域性容器有可執行G佇列,排程器的自由G列表,本地P的可執行G佇列以及本地P的自由G列表。 執行時系統創建出的任何G都回存在於全域性G列表中,而其餘的4個列表則只存放在當前作用域的所有特定 的狀態的G。注意,這裡的兩個可執行G列表中的G都擁有幾乎平等的執行機會。由於這種平等性的存在,所以 我們無需關心哪類可執行的G會進入到哪一個佇列中,不過。可以順便提一下,從Gsyscall狀態和Ggstop狀態轉出 的G,都會被放入排程器的可執行G佇列,而被執行時系統初始化的G,都會被放入本地P的可執行G佇列。至於 從Gwaiting狀態轉出的G,除了因進行網路I/O而陷入等待的G之外,都會被存放到本地P的可執行G佇列。此外, 我們之前說過,對runtine.GOMAXPROCS函式的呼叫,可能會導致執行時系統清空排程器的可執行G佇列。其中的 所有G都會被均勻地放入到全域性P列表這種的各個P的可執行G對了當中。另一方面在G轉入Gdead狀態後,首先會被 放入本地P的自由G列表,而在執行時系統需要用自由G封裝go函式的時候,也會嘗試從本地P的自由G列表中獲取。 排程器的自由G列表只是起到了一個暫存自由G的作用。

與M和P相關的非全域性容器分別是排程器的空閒M列表和排程器的空閒P列表。這兩個列表都被用於存放暫時不被 使用的元素的例項。在執行時系統有需要的時候,會從中獲取i相應的元素的例項重新啟動該它。