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相應的元素的實例重新啟動該它。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM