您好,登錄后才能下訂單哦!
Go語言中goroutine的調度原理是什么,很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細講解,有這方面需求的人可以來學習下,希望你能有所收獲。
在講goroutine的調度原理之前,有些與操作系統相關的知識,我們需要先知道,例如:
1.什么是并發?
并發:兩個或兩個以上的任務在一段時間內被執行。我們并不關心這些任務是否在同一時刻執行,我們只是知道,這些任務在這一段時間能能夠都被執行,當然這一段時間可以很長,也可以很短。
2.并發的最小并發單位是什么?
進程是計算機資源分配最小的單位,是CPU分配資源的基本單位,具有獨立的內存。
線程是計算機調度最小的單位,也是程序執行的最小單位,是在進程中的,一個進程往往會有一個到多個線程。
3.計算機是如何實現并發的?
計算機的分時調用是并發的根本,CPU通過快速的切換作業來執行不同的作業,基本的調度單位在執行的時候可以被阻塞掉,此時就會將CPU資源讓出來,等到該調度單位再次被喚醒的時候,又可以使用CPU資源,而操作系統保證了整個的調度過程。
除此之外,關于goroutine的調度原理,我們需要弄清楚下面幾個問題。
1.goroutine是什么?
Goroutine:是Go里的一種輕量級線程——協程。
1)相對線程,協程的優勢就在于它非常輕量級,進行上下文切換的代價非常的小。
2)對于一個goroutine ,每個結構體G中有一個sched的屬性就是用來保存它上下文的。這樣,goroutine 就可以很輕易的來回切換。
3)由于其上下文切換在用戶態下發生,根本不必進入內核態,所以速度很快。而且只有當前goroutine 的 PC, SP等少量信息需要保存。
4)在Go語言中,每一個并發的執行單元為一個goroutine。
Go 語言中的goroutine并發, 采用的是CSP(communicating sequential processes)并發模型,講究的是以通訊的方式來進行數據共享,是通過goroutine配合channel的方式來實現的。(備注:這部分知識后續單獨整理一章。)
2.既然它是比線程還小的粒度,那么它與線程有什么關系?
Go語言的線程模型就是一種特殊的兩級線程模型,如下所示:
兩級線程模型的實現非常復雜,和內核級線程模型類似,一個進程中可以對應多個內核級線程,但是進程中的線程不和內核線程一一對應;這種線程模型會先創建多個內核級線程,然后用自身的用戶級線程去對應創建的多個內核級線程,自身的用戶級線程需要本身程序去調度,內核級的線程交給操作系統內核去調度。
我們先來看下,Go線程實現了MPG模型:
S(Sched):結構就是調度器,它維護有存儲M和G的隊列以及調度器的一些狀態信息等。
M(Machine):一個M直接關聯了一個內核線程。
P(processor):代表了M所需的上下文環境,也是處理用戶級代碼邏輯的處理器。G(Goroutine):其實本質上也是一種輕量級的線程。
它們的關系如下所示:
介紹:
一個M會關聯兩個東西,一個是內核線程,一個是可執行的進程。
一個上下文P會有兩類Goroutine,一類是正在運行的,圖中的藍色G;一類是正在排隊的,圖中灰色G,這個會存儲在該進程中的runqueue里面。
這里的上下文P的數量也表示的是Goroutinue運行的數量,一般設置為幾個,機器中就會并發運行幾個。當然這里P的數量是可以設置的,通過環境變量GOMAXPROCS的值,或者通過運行時調用函數runtime.GOMAXPROCS()進行設置,最大值是256。
有了上面的知識,我們知道了Goroutine的一些基本概念,但是我們還是不知道,Go的并發是如何調度的。而這一個話題,就需要我們將Goroutine的幾種場景(創建、銷毀和運行)做拆分。
1.在執行go語句之前,我們看下程序都做了哪些準備,也就是程序的初始化啟動流程是什么樣子的?
上面的代碼,有三個點非常關鍵,分別是runtime.schedinit,runtime.main,runtime.mstart
Step1: runtime.schedinit:這一步是調度器的初始化操作,它會設置GOMAXPROCS的大小,這里的大小不能超過它的上限256,并創建設置好對應數量的P,當然這些P都處于閑置狀態;然后,將這些創建好的P都存放到sched中pidle所關聯的閑置列表中。
Step2: 程序會繼續執行runtime.newproc來創建程序的第一個goroutine,而這個goroutine會執行runtime.main也就是我們看到的main函數,在這之后main會主動創建一個內核線程M,這個M只用來做系統監控用,這個內核線程與程序中goroutinue的調度有關系。
Step3:在runtime.mstart之后,程序就開始執行了,如果后續需要創建goroutine,就會調用go語句來創建。
2.goroutine創建流程是什么樣子的?
在調用go func()的時候,會調用runtime.newproc來創建一個goroutine,這個goroutine會新建一個自己的棧空間,同時在G的sched中維護棧地址與程序計數器這些信息(備注:這些數據在goroutine被調度的時候會被用到。準確的說該goroutine在放棄cpu之后,下一次在重新獲取cpu的時候,這些信息會被重新加載到cpu的寄存器中。)
創建好的這個goroutine會被放到,它所對應的內核線程M所使用的上下文P中的runqueue中。等待調度器來決定何時取出該goroutine并執行,通常調度是按時間順序被調度的,這個隊列是一個先進先出的隊列。
3.新建的這些goroutine是如何被調度的呢?
goroutine在創建好了之后,調度器會決定何時執行這個goroutine,這個過程就叫做調度。
新建好的goroutine,最開始都會存儲在某一個線程M,所關聯的上下文P的runqueue中,但是在后續的調度中,有些goroutine因為調用了runtime.gosched,會被放到全局隊列中。
線程M的選擇過程,按照下面的順序執行:
1.從M對應的P中的runqueue中取出goroutine,來執行,沒有的話,執行2。
2.從全局隊列里面嘗試取出一個goroutine來執行,有的話,執行!沒有的話,執行3。
3.從其他的線程M的P中,偷出一些goroutine來執行,偷失敗了,執行4。(備注:這里偷的話,一偷就偷一半,使用的算法叫做work stealing。)
4.線程M發現無事可做,就去休息了,也就是線程的sleep,它等待被喚醒。
4.運行中的goroutine是怎么停止的呢?一旦被停止了的話,那排隊在它后面的goutinue該怎么辦?
講完了goroutine的調度之后,我們便要考慮一個問題,正在被執行的goroutine何時停止,停止了之后會發生什么?而掛在M對應的P后面的runqueue中的goroutine該怎么辦?
情況1:runtime·park
當調用了runtime·park函數之后,goroutine會被設置成waiting狀態,線程M會放棄它自身關聯的上下文P,而系統會分配一個新的線程M1來接管這個上下文P,(備注:當然這里面的M1也有可能是本來就創建好的,處于閑置狀態中的)。
原來的線程M0則會與上下文斷開連接,M0因為無事可做,就去sleep了,等待下次被喚醒。如下圖所示:
channel的讀寫操作,定時器中,網絡poll等都有可能park goroutine。
情況2:runtime·gosched
調用runtime·gosched函數也可以讓當前goroutine放棄cpu,這種情況下會將goroutine設置成runnable,放置到全局隊列中。備注:這個也就是為什么全局變量的queue里面會有goroutine的原因。
5.goroutine被喚醒之后,會做什么?
goroutine處于waiting狀態的話,在調用runtime·ready函數之后,會被喚醒,喚醒的goroutine會被重新放到,M對應的上下文所對應的runqueue中,等待被調度。
看完上述內容是否對您有幫助呢?如果還想對相關知識有進一步的了解或閱讀更多相關文章,請關注億速云行業資訊頻道,感謝您對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。