go 調度機制簡介


goroutine是go中最重要的功能之一,正是因為有了goroutine這樣強大的工具,go在並發方面表現的特別優秀。

那么goroutine和普通的線程和協程有什么區別呢?首先,我們需要明白線程和協程的區別,線程是內核態的,而協程是用戶態的。什么意思呢?就是說線程之間的切換主要由內核去調度,而協程之間的切換則需要用戶去操作。線程切換需要保存上下文信息,切換到另一個線程,過段時間,恢復到之前的線程繼續執行。cpu時間片的讓渡,上下文的保存等等復雜操作都是由內核實現的,程序員不需要關注其中的細節。對程序員更加友好。但是為了支持這些操作,線程需要使用大量的資源。所以一個進程之間只能支持少量的線程,一般幾個,十幾個就會將資源耗盡。而協程則不同,它將協程之間的調度交給程序員去處理,優秀的程序員可以通過各種操作降低協程之間上下文切換資源占用,處理切換時機等等。對程序員的水平要求更高,由於調度由用戶控制,那么使用的資源相對來說會更少,所以一個進程可以啟動的協程數量比線程更多。

goroutine結合了線程和協程的優點。主要表現在,從資源占用上看,goroutine更像是協程,占用的資源都很少,支持一個進程開千個萬個goroutine。而從切換角度來看,goroutine更像是線程,不需要用戶實現goroutine之間的調度。goroutine之間的調度不由內核來操作,也不由用戶操作。由go自己來操作,go中自己實現了調度器。

簡單的說,goroutine既擁有協程的優點,對系統資源的占用低,又擁有線程的優點,用戶不需要實現協程的調度。只需要學習如何使用go自身的調度器即可。

goroutine中很重要的一個概念是

不要通過共享內存來通信,要通過通信來共享內存

通過通信來共享內存,降低了對內存的占用,go中使用channel通道來實現通信。這里不詳細解釋。

我們主要介紹下go 的調度機制,學會了go的調度機制,可以幫助我們學習和理解如何正確使用goroutine,定位和解決goroutine中出現的問題。

整個調度模型由 Goroutine/Processor/Machine 以及全局調度信息 sched 組成。

上述是個人的理解,有不正確的地方歡迎指出。

調度機制已經有很多人寫了,寫的也很簡單明了,就直接轉載過來。后面有時間會去閱讀源碼,如果下面的某些闡述與源碼有沖突,后續會改正。

GO並發模型的實現原理

我們先從線程講起,無論語言層面何種並發模型,到了操作系統層面,一定是以線程的形態存在的。而操作系統根據資源訪問權限的不同,體系架構可分為用戶空間和內核空間;內核空間主要操作訪問CPU資源、I/O資源、內存資源等硬件資源,為上層應用程序提供最基本的基礎資源,用戶空間呢就是上層應用程序的固定活動空間,用戶空間不可以直接訪問資源,必須通過“系統調用”、“庫函數”或“Shell腳本”來調用內核空間提供的資源。

我們現在的計算機語言,可以狹義的認為是一種“軟件”,它們中所謂的“線程”,往往是用戶態的線程,和操作系統本身內核態的線程(簡稱KSE),還是有區別的。

線程模型的實現,可以分為以下幾種方式:

用戶級線程模型

 

如圖所示,多個用戶態的線程對應着一個內核線程,程序線程的創建、終止、切換或者同步等線程工作必須自身來完成。它可以做快速的上下文切換。缺點是不能有效利用多核CPU。

內核級線程模型

 

這種模型直接調用操作系統的內核線程,所有線程的創建、終止、切換、同步等操作,都由內核來完成。一個用戶態的線程對應一個系統線程,它可以利用多核機制,但上下文切換需要消耗額外的資源。C++就是這種。

兩級線程模型

 

這種模型是介於用戶級線程模型和內核級線程模型之間的一種線程模型。這種模型的實現非常復雜,和內核級線程模型類似,一個進程中可以對應多個內核級線程,但是進程中的線程不和內核線程一一對應;這種線程模型會先創建多個內核級線程,然后用自身的用戶級線程去對應創建的多個內核級線程,自身的用戶級線程需要本身程序去調度,內核級的線程交給操作系統內核去調度。

M個用戶線程對應N個系統線程,缺點增加了調度器的實現難度。

Go語言的線程模型就是一種特殊的兩級線程模型(GPM調度模型)。

以上轉載自https://www.jianshu.com/p/4afa0679851d

調度器

主要基於三個基本對象上,G,M,P(定義在源碼的src/runtime/runtime.h文件中)

1.     G代表一個goroutine對象,每次go調用的時候,都會創建一個G對象

2.     M代表一個線程,每次創建一個M的時候,都會有一個底層線程創建;所有的G任務,最終還是在M上執行

3.     P代表一個處理器,每一個運行的M都必須綁定一個P,就像線程必須在么一個CPU核上執行一樣

P的個數就是GOMAXPROCS(最大256),啟動時固定的,一般不修改; M的個數和P的個數不一定一樣多(會有休眠的M或者不需要太多的M)(最大10000);每一個P保存着本地G任務隊列,也有一個全局G任務隊列;

全局G任務隊列會和各個本地G任務隊列按照一定的策略互相交換(滿了,則把本地隊列的一半送給全局隊列)

P是用一個全局數組(255)來保存的,並且維護着一個全局的P空閑鏈表

每次go調用的時候,都會:

1.     創建一個G對象,加入到本地隊列或者全局隊列

2.     如果還有空閑的P,則創建一個M

3.     M會啟動一個底層線程,循環執行能找到的G任務

4.     G任務的執行順序是,先從本地隊列找,本地沒有則從全局隊列找(一次性轉移(全局G個數/P個數)個,再去其它P中找(一次性轉移一半),

5.     以上的G任務執行是按照隊列順序(也就是go調用的順序)執行的。

對於上面的第2-3步,創建一個M,其過程:

1.     先找到一個空閑的P,如果沒有則直接返回,(這個地方就保證了進程不會占用超過自己設定的cpu個數)

2.     調用系統api創建線程,不同的操作系統,調用不一樣,其實就是和c語言創建過程是一致的,(windows用的是CreateThread,linux用的是clone系統調用)

3.     然后創建的這個線程里面才是真正做事的,循環執行G任務

那就會有個問題,如果一個系統調用或者G任務執行太長,他就會一直占用這個線程,由於本地隊列的G任務是順序執行的,其它G任務就會阻塞了,怎樣中止長任務的呢?

這樣滴,啟動的時候,會專門創建一個線程sysmon,用來監控和管理,在內部是一個循環:

1.     記錄所有P的G任務計數schedtick,(schedtick會在每執行一個G任務后遞增)

2.     如果檢查到 schedtick一直沒有遞增,說明這個P一直在執行同一個G任務,如果超過一定的時間(10ms),就在這個G任務的棧信息里面加一個標記

3.     然后這個G任務在執行的時候,如果遇到非內聯函數調用,就會檢查一次這個標記,然后中斷自己,把自己加到隊列末尾,執行下一個G

4.     如果沒有遇到非內聯函數(有時候正常的小函數會被優化成內聯函數)調用的話,那就慘了,會一直執行這個G任務,直到它自己結束;如果是個死循環,並且GOMAXPROCS=1的話,程序會掛死。

對於一個G任務,中斷后的恢復過程:

1.     中斷的時候將寄存器里的棧信息,保存到自己的G對象里面

2.     當再次輪到自己執行時,將自己保存的棧信息復制到寄存器里面,這樣就接着上次之后運行了。
————————————————
版權聲明:本文為CSDN博主「正版兩只羊」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/liangzhiyang/article/details/52669851

總結,執行go程序時,從main開始,main goroutine創建一個新的M,綁定一個P。如果main函數里有其他的goroutine,查詢是否有空閑的P,如果有,創建一個新的M,加入新P的本地隊列中,綁定其他空閑的P。如果沒有空閑的P,那么將G加入全局隊列中。每次執行時,循環執行本地隊列任務(本地隊列和全局隊列會交換任務),然后執行全局任務,都沒有的話,去其他P里面偷,偷一半。

以銀行辦理業務為例,客戶代表G,窗口代表P,M代表排隊通道。sched代表一個全局的排隊通道。來了一個客戶,將安排到一個空閑的排隊通道M里,這個排隊通道M綁定一個窗口P(每個排隊通道都要綁定一個窗口P,一個窗口P可以有多個排隊通道M,N:M)。如果來新客戶,看下有沒有空閑的窗口P,有的話,開一個新的排隊通道M,將新客戶安排到這個排隊通道里。如果沒有空閑的窗口P,此時,每個排隊通道M都有客戶G,如果排隊通道M沒有滿,那么G安排到一個有空閑位置的M。如果所有的M都滿了,那么安排客戶G到全局排隊通道sched。執行過程中,如果某個客戶的任務執行太久,(使用任務計數schedtick判斷),將客戶安排到排隊通道M末尾。(重新排隊)。如果某個窗口P對應的排隊通道M都空了(一個窗口P可能有多個排隊通道M),那么看下全局排隊通道有沒有客戶G,有就過來(M的本地任務和sched全局任務有定期的交換策略),沒有的話,去其他窗口P里面偷一半客戶G過來執行。

不知道上面的例子描述的恰當不恰當,如果有錯誤歡迎指出。


免責聲明!

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



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