亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Golang中的調(diào)度器GPM模型詳解

 更新時間:2025年05月16日 10:01:50   作者:二六八  
這篇文章主要介紹了Golang中的調(diào)度器GPM模型,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教

Golang 調(diào)度器 GPM模型

1 多進(jìn)程/線程時代有了調(diào)度器需求

在多進(jìn)程/多線程的操作系統(tǒng)中,就解決了阻塞的問題,因為一個進(jìn)程阻塞cpu可以立刻切換到其他進(jìn)程中去執(zhí)行,而且調(diào)度cpu的算法可以保證在運行的進(jìn)程都可以被分配到cpu的運行時間片。這樣從宏觀來看,似乎多個進(jìn)程是在同時被運行。

上圖為一個CPU通過調(diào)度器切換CPU時間軸的情景。如果未來滿足宏觀上每個進(jìn)程/線程是一起執(zhí)行的,則CPU必須切換,每個進(jìn)程會被分配到一個時間片中。

但新的問題就又出現(xiàn)了,進(jìn)程擁有太多的資源,進(jìn)程的創(chuàng)建、切換、銷毀,都會占用很長的時間,CPU雖然利用起來了,但如果進(jìn)程過多,CPU有很大的一部分都被用來進(jìn)行進(jìn)程調(diào)度了。如下圖所示:

對于Linux操作系統(tǒng)來言,CPU對進(jìn)程和線程的態(tài)度是一樣的,如圖1.3所示,如果系統(tǒng)的CPU數(shù)量過少,而進(jìn)程/線程數(shù)量比較龐大,則相互切換的頻率也就會很高,其中中間的切換成本越來越大。這一部分的性能消耗實際上是沒有做在對程序有用的計算算力上,所以盡管線程看起來很美好,但實際上多線程開發(fā)設(shè)計會變得更加復(fù)雜,開發(fā)者要考慮很多同步競爭的問題,如鎖、資源競爭、同步?jīng)_突等。

2 協(xié)程來提高CPU利用率

那么如何才能提高CPU的利用率呢?多進(jìn)程、多線程已經(jīng)提高了系統(tǒng)的并發(fā)能力,但是在當(dāng)今互聯(lián)網(wǎng)高并發(fā)場景下,為每個任務(wù)都創(chuàng)建一個線程是不現(xiàn)實的,因為這樣就會出現(xiàn)極大量的線程同時運行,不僅切換頻率高,也會消耗大量的內(nèi)存:進(jìn)程虛擬內(nèi)存會占用4GB(32位操作系統(tǒng)),而線程也要大約4MB。大量的進(jìn)程或線程出現(xiàn)了以下兩個新的問題。

  • (1)高內(nèi)存占用。
  • (2)調(diào)度的高消耗CPU。

工程師發(fā)現(xiàn)其實可以把一個線程分為“內(nèi)核態(tài)”和“用戶態(tài)”兩種形態(tài)的線程。所謂用戶態(tài)線程就是把內(nèi)核態(tài)的線程在用戶態(tài)實現(xiàn)了一遍而已,目的是更輕量化(更少的內(nèi)存占用、更少的隔離、更快的調(diào)度)和更高的可控性(可以自己控制調(diào)度器)。用戶態(tài)中的所有東西內(nèi)核態(tài)都看得見,只是對于內(nèi)核而言用戶態(tài)線程只是一堆內(nèi)存數(shù)據(jù)而已。

一個用戶態(tài)線程必須綁定一個內(nèi)核態(tài)線程,但是CPU并不知道有用戶態(tài)線程的存在,它只知道它運行的是一個內(nèi)核態(tài)線程(Linux的PCB進(jìn)程控制塊),如下圖所示:

如果將線程再進(jìn)行細(xì)化,內(nèi)核線程依然叫 線程(Thread) ,而用戶線程則叫 協(xié)程(Co-routine) 。操作系統(tǒng)層面的線程就是所謂的內(nèi)核態(tài)線程,用戶態(tài)線程則多種多樣,只要能滿足在同一個內(nèi)核線程上執(zhí)行多個任務(wù),例如Co-routine、Go的Goroutine、C#的Task等。

既然一個協(xié)程可以綁定一個線程,那么能不能多個協(xié)程綁定一個或者多個線程呢?接下來有3種協(xié)程和線程的映射關(guān)系,它們分別是 N : 1 關(guān)系、 1 : 1 關(guān)系和 M : N 關(guān)系。

3 N比1關(guān)系

N個協(xié)程綁定1個線程,優(yōu)點就是協(xié)程在用戶態(tài)線程即完成切換,不會陷入內(nèi)核態(tài),這種切換非常輕量快速,但缺點也很明顯,1個進(jìn)程的所有協(xié)程都綁定在1個線程上,如圖所示。

N:1關(guān)系面臨的幾個問題如下:

  • (1) 某個程序用不了硬件的多核加速能力。
  • (2) 某一個協(xié)程阻塞,會造成線程阻塞,本進(jìn)程的其他協(xié)程都無法執(zhí)行了,進(jìn)而導(dǎo)致沒有任何并發(fā)能力。

4 1比1關(guān)系

1個協(xié)程綁定1個線程,這種方式最容易實現(xiàn)。協(xié)程的調(diào)度都由CPU完成了,雖然不存在N:1的缺點,但是協(xié)程的創(chuàng)建、刪除和切換的代價都由CPU完成,成本和代價略顯昂貴。協(xié)程和線程的1:1關(guān)系如圖所示。

5 M比N關(guān)系

M個協(xié)程綁定1個線程,是 N: 11 : 1 類型的結(jié)合,克服了以上兩種模型的缺點,但實現(xiàn)起來最為復(fù)雜。同一個調(diào)度器上掛載M個協(xié)程,調(diào)度器下游則是多個CPU核心資源。協(xié)程跟線程是有區(qū)別的,線程由CPU調(diào)度是搶占式的,協(xié)程由用戶態(tài)調(diào)度是協(xié)作式的,一個協(xié)程讓出CPU后,才執(zhí)行下一個協(xié)程,所以針對 M : N 模型的中間層的調(diào)度器設(shè)計就變得尤為重要,提高線程和協(xié)程的綁定關(guān)系和執(zhí)行效率也變?yōu)椴煌Z言在設(shè)計調(diào)度器時的優(yōu)先目標(biāo)。

6 Go語言的協(xié)程goroutine

Go語言為了提供更容易使用的并發(fā)方法,使用了Goroutine和Channel。Goroutine來自協(xié)程的概念,讓一組可復(fù)用的函數(shù)運行在一組線程之上,即使有協(xié)程阻塞,該線程的其他協(xié)程也可以被runtime調(diào)度,從而轉(zhuǎn)移到其他可運行的線程上。最關(guān)鍵的是,程序員看不到這些底層的細(xì)節(jié),這就降低了編程的難度,提供了更容易的并發(fā)。

在Go語言中,協(xié)程被稱為Goroutine,它非常輕量,一個Goroutine只占幾KB,并且這幾KB就足夠Goroutine運行完,這就能在有限的內(nèi)存空間內(nèi)支持大量Goroutine,從而支持更多的并發(fā)。雖然一個Goroutine的棧只占幾KB,但實際是可伸縮的,如果需要更多內(nèi)存,則runtime會自動為Goroutine分配。

Goroutine的特點,占用內(nèi)存更小(幾KB)和調(diào)度更靈活(runtime調(diào)度)。

7 被廢棄的goroutine調(diào)度器

Go語言目前使用的調(diào)度器是2012年重新設(shè)計的,因為之前的調(diào)度器性能存在問題,所以使用4年就被廢棄了,那么先來分析一下被廢棄的調(diào)度器是如何運作的。

通常用符號G表示Goroutine,用M表示線程。接下來有關(guān)調(diào)度器的內(nèi)容均采用圖1.8所示的符號來統(tǒng)一表達(dá)。

早期的調(diào)度器是基于M:N的基礎(chǔ)上實現(xiàn)的,圖1.9是一個概要圖形,所有的協(xié)程,也就是G都會被放在一個全局的Go協(xié)程隊列中,在全局隊列的外面由于是多個M的共享資源,所以會加上一個用于同步及互斥作用的鎖。

M想要執(zhí)行、放回G都必須訪問全局G隊列,并且M有多個,即多線程訪問同一資源需要加鎖進(jìn)行保證互斥/同步,所以全局G隊列是由互斥鎖進(jìn)行保護(hù)的。

不難分析出來,老調(diào)度器有以下幾個缺點:

  • (1) 創(chuàng)建、銷毀、調(diào)度G都需要每個M獲取鎖,這就形成了激烈的鎖競爭。
  • (2) M轉(zhuǎn)移G會造成延遲和額外的系統(tǒng)負(fù)載。例如當(dāng)G中包含創(chuàng)建新協(xié)程的時候,M創(chuàng)建了G′,為了繼續(xù)執(zhí)行G,需要把G′交給M2(假如被分配到)執(zhí)行,也造成了很差的局部性,因為G′和G是相關(guān)的,最好放在M上執(zhí)行,而不是其他M2,如圖1.10所示
  • (3) 系統(tǒng)調(diào)用(CPU在M之間的切換)導(dǎo)致頻繁的線程阻塞和取消阻塞操作增加了系統(tǒng)開銷。

8 Goroutine調(diào)度器的GMP模型的設(shè)計思想

面對之前調(diào)度器的問題,Go設(shè)計了新的調(diào)度器。在新調(diào)度器中,除了 M(線程)G(協(xié)程) ,又引進(jìn)了 P(處理器) 。

處理器包含了運行Goroutine的資源,如果線程想運行Goroutine,必須先獲取P,P中還包含了可運行的G隊列。

9 GPM模型

在Go中,線程是運行Goroutine的實體,調(diào)度器的功能是把可運行的Goroutine分配到工作線程上。

在GPM模型中有以下幾個重要的概念,如圖1.12所示。

  • (1)全局隊列(Global Queue): 存放等待運行的G。全局隊列可能被任意的P去獲取里面的G,所以全局隊列相當(dāng)于整個模型中的全局資源,那么自然對于隊列的讀寫操作是要加入互斥動作的。
  • (2)P的本地隊列: 同全局隊列類似,存放的也是等待運行的G,但存放的數(shù)量有限,不超過256個。新建G′時,G′優(yōu)先加入P的本地隊列,如果隊列滿了,則會把本地隊列中一半的G移動到全局隊列。
  • (3)P列表: 所有的P都在程序啟動時創(chuàng)建,并保存在數(shù)組中,最多有GOMAXPROCS(可配置)個。
  • (4)M: 線程想運行任務(wù)就得獲取P,從P的本地隊列獲取G,當(dāng)P隊列為空時,M也會嘗試從全局隊列獲得一批G放到P的本地隊列,或從其他P的本地隊列“偷”一半放到自己P的本地隊列。M運行G,G執(zhí)行之后,M會從P獲取下一個G,不斷重復(fù)下去。

Goroutine調(diào)度器和OS調(diào)度器是通過M結(jié)合起來的,每個M都代表了1個內(nèi)核線程,OS調(diào)度器負(fù)責(zé)把內(nèi)核線程分配到CPU的核上執(zhí)行。

10 有關(guān)P和M個數(shù)的問題

  • (1) P的數(shù)量由啟動時環(huán)境變量 $GOMAXPROCS 或者由 runtime 的方法 GOMAXPROCS( ) 決定。這意味著在程序執(zhí)行的任意時刻都只有 $GOMAXPROCS 個 Goroutine在 同時運行。
  • (2)M的數(shù)量由Go語言本身的限制決定,Go程序啟動時會設(shè)置M的最大數(shù)量,默認(rèn)為10000個,但是內(nèi)核很難支持這么多的線程數(shù),所以這個限制可以忽略。runtime/deBug 中的 SetMaxThreads( ) 函數(shù)可設(shè)置M的最大數(shù)量,當(dāng)一個M阻塞了時會創(chuàng)建新的M。

M與P的數(shù)量沒有絕對關(guān)系,一個M阻塞,P就會去創(chuàng)建或者切換另一個M,所以,即使P的默認(rèn)數(shù)量是1,也有可能會創(chuàng)建很多個M出來。

11 有關(guān)P和M何時被創(chuàng)建

  • (1) P創(chuàng)建的時機(jī)在確定了P的最大數(shù)量n后,運行時系統(tǒng)會根據(jù)這個數(shù)量創(chuàng)建n個P。
  • (2) M創(chuàng)建的時機(jī)是在當(dāng)沒有足夠的M來關(guān)聯(lián)P并運行其中可運行的G的時候。例如所有的M此時都阻塞住了,而P中還有很多就緒任務(wù),就會去尋找空閑的M,如果此時沒有空閑的M,就會去創(chuàng)建新的M。

12 調(diào)度器的設(shè)計策略

策略一:復(fù)用線程

避免頻繁地創(chuàng)建、銷毀線程,而是對線程的復(fù)用。

1)偷取(Work Stealing)機(jī)制

當(dāng)本線程無可運行的G時,嘗試從其他線程綁定的P偷取G,而不是銷毀線程,如圖1.13所示

這里需要注意的是,偷取的動作一定是由P發(fā)起的,而非M,因為P的數(shù)量是固定的,如果一個M得不到一個P,那么這個M是沒有執(zhí)行的本地隊列的,更談不上向其他的P隊列偷取了。

2)移交(Hand Off)機(jī)制

當(dāng)本線程因為G進(jìn)行系統(tǒng)調(diào)用阻塞時,線程會釋放綁定的P,把P轉(zhuǎn)移給其他空閑的線程執(zhí)行,如圖1.14所示,此時若在M1的GPM組合中,G1正在被調(diào)度,并且已經(jīng)發(fā)生了阻塞,則這個時候就會觸發(fā)移交的設(shè)計機(jī)制。GPM模型為了更大程度地利用M和P的性能,不會讓一個P永遠(yuǎn)被一個阻塞的G1耽誤之后的工作,所以遇見這種情況的時候,移交機(jī)制的設(shè)計理念是應(yīng)該立刻將此時的P釋放出來

如圖1.15所示,為了釋放P,所以將P和M1、G1分離,M1由于正在執(zhí)行當(dāng)前的G1,全部的程序??臻g均在M1中保存,所以M1此時應(yīng)該與G1一同進(jìn)入阻塞的狀態(tài),但是已經(jīng)被釋放的P需要跟另一個M進(jìn)行綁定,所以就會選擇一個M3(如果此時沒有M3,則會創(chuàng)建一個新的或者喚醒一個正在睡眠的M)進(jìn)行綁定,這樣新的P就會繼續(xù)工作,接收新的G或者從其他的隊列中實施偷取機(jī)制。

策略二:利用并行

GOMAXPROCS 設(shè)置P的數(shù)量,最多有 GOMAXPROCS 個線程分布在多個CPU上同時運行。 GOMAXPROCS 也限制了并發(fā)的程度,例如 GOMAXPROCS=核數(shù)/2 ,表示最多利用一半的CPU核進(jìn)行并行。

策略三:搶占

在Co-routine中要等待一個協(xié)程主動讓出CPU才執(zhí)行下一個協(xié)程,在Go中,一個Goroutine最多占用CPU 10ms,防止其他Goroutine無資源可用,這就是Goroutine不同于Co-routine的一個地方。

  • Co-routine(C語言中的協(xié)程),用戶態(tài)線程。
  • coroutine 是基于 ucontext 的一個 C 語言協(xié)程庫實現(xiàn)

策略四:全局G隊列

在新的調(diào)度器中依然有全局G隊列,但功能已經(jīng)被弱化了,當(dāng)M執(zhí)行偷取,但從其他P偷不到G時,它可以從全局G隊列獲取G。

13 go func() 調(diào)度流程

如果執(zhí)行一行代碼 go func( ) ,則在GPM模型上的概念里會執(zhí)行哪些操作。

(1)通過 go func( ) 創(chuàng)建一個Goroutine,

(2)有兩個存儲G的隊列,一個是局部調(diào)度器P的本地隊列,另一個是全局G隊列。新創(chuàng)建的G會先保存在P的本地隊列中,如果P的本地隊列已經(jīng)滿了,就會保存在全局的隊列中,如圖1.19所示。

(3)G只能運行在M中,一個M必須持有一個P,M與P是1:1的關(guān)系。M會從P的本地隊列彈出一個可執(zhí)行狀態(tài)的G來執(zhí)行,如果P的本地隊列為空,則會從全局隊列進(jìn)行獲取,如果從全局隊列獲取不到,則會向其他的MP組合偷取一個可執(zhí)行的G來執(zhí)行,如圖1.20所示。

(4)一個M調(diào)度G執(zhí)行的過程是一個循環(huán)機(jī)制,如圖1.21所示。

(5)當(dāng)M執(zhí)行某一個G時如果發(fā)生了syscall或者其余阻塞操作,則M會阻塞,如果當(dāng)前有一些G在執(zhí)行,runtime則會把這個線程M從P中移除(Detach),然后創(chuàng)建一個新的操作系統(tǒng)線程(如果有空閑的線程可用就復(fù)用空閑線程)來服務(wù)于這個P。

(6)當(dāng)M系統(tǒng)調(diào)用結(jié)束時,這個G會嘗試獲取一個空閑的P執(zhí)行,并放入這個P的本地隊列。如果獲取不到P,則這個線程M會變成休眠狀態(tài),加入空閑線程中,然后這個G會被放入全局隊列中。

14 調(diào)度器的生命周期

在Go語言調(diào)度器的GPM模型中還有兩個比較特殊的角色,它們分別是M0和G0。

M0

  • (1)啟動程序后的編號為0的主線程。
  • (2)在全局命令runtime.m0中,不需要在heap堆上分配。
  • (3)負(fù)責(zé)執(zhí)行初始化操作和啟動第1個G。
  • (4)啟動第1個G后,M0就和其他的M一樣了。

G0

  • (1)每次啟動一個M,創(chuàng)建的第1個Goroutine就是G0。
  • (2)G0僅用于負(fù)責(zé)調(diào)度G。
  • (3)G0不指向任何可執(zhí)行的函數(shù)。
  • (4)每個M都會有一個自己的G0。
  • (5)在調(diào)度或系統(tǒng)調(diào)度時,會使用M切換到G0,再通過G0調(diào)度。
  • (6)M0的G0會放在全局空間。

一個Goroutine的創(chuàng)建周期如果加上M0和G0的角色,則整體的流程如圖1.24所示。

下面跟蹤一段代碼,對調(diào)度器里面的結(jié)構(gòu)做一個分析,代碼如下:

package main
 
import "fmt"
 
func main() {
    fmt.Println("Hello world")
}

整體的分析過程如下:

  • (1)runtime創(chuàng)建最初的線程 M0Goroutine G0 ,并把二者關(guān)聯(lián)。
  • (2)調(diào)度器初始化:初始化M0、棧、垃圾回收,以及創(chuàng)建和初始化由 GOMAXPROCSP 構(gòu)成的 P列表 ,如圖1.25所示。
  • (3)示例代碼中的 main( ) 函數(shù)是 main.main ,runtime中也有1個main()函數(shù) runtime.main ,代碼經(jīng)過編譯后, runtime.main 會調(diào)用 main.main ,程序啟動時會為 runtime.main 創(chuàng)建Goroutine,稱為 Main Goroutine ,然后把 Main Goroutine 加入P的本地隊列。
  • (4)啟動M0,M0已經(jīng)綁定了P,會從P的本地隊列獲取G,并獲取 Main Goroutine
  • (5)G擁有棧,M根據(jù)G中的棧信息和調(diào)度信息設(shè)置運行環(huán)境。
  • (6)M運行G。
  • (7)G退出,再次回到M獲取可運行的G,這樣重復(fù)下去,直到 main.main 退出, runtime.main 執(zhí)行Defer和Panic處理,或調(diào)用 runtime.exit 退出程序。

調(diào)度器的生命周期幾乎占滿了一個Go程序的一生, runtime.main 的Goroutine執(zhí)行之前都是為調(diào)度器做準(zhǔn)備工作, runtime.main 的Goroutine運行才是調(diào)度器的真正開始,直到 runtime.main 結(jié)束而結(jié)束。

總結(jié)

以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • Go語言中strings和strconv包示例代碼詳解

    Go語言中strings和strconv包示例代碼詳解

    這篇文章主要介紹了Go語言中strings和strconv包示例代碼詳解 ,非常不錯,具有一定的參考借鑒價值,需要的朋友可以參考下
    2018-11-11
  • golang中的空slice案例

    golang中的空slice案例

    這篇文章主要介紹了golang中的空slice案例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-04-04
  • Go語言計算兩個經(jīng)度和緯度之間距離的方法

    Go語言計算兩個經(jīng)度和緯度之間距離的方法

    這篇文章主要介紹了Go語言計算兩個經(jīng)度和緯度之間距離的方法,涉及Go語言相關(guān)數(shù)學(xué)函數(shù)的使用技巧,具有一定參考借鑒價值,需要的朋友可以參考下
    2015-02-02
  • Golang實現(xiàn)優(yōu)雅的將struct轉(zhuǎn)換為map

    Golang實現(xiàn)優(yōu)雅的將struct轉(zhuǎn)換為map

    在項目實踐中,有時候我們需要將struct結(jié)構(gòu)體轉(zhuǎn)為map映射表,然后基于map做數(shù)據(jù)裁剪或操作。那么下面我來介紹下常用的兩種轉(zhuǎn)換方式,希望對大家有所幫助
    2023-01-01
  • 一文詳解go中如何實現(xiàn)定時任務(wù)

    一文詳解go中如何實現(xiàn)定時任務(wù)

    定時任務(wù)是指按照預(yù)定的時間間隔或特定時間點自動執(zhí)行的計劃任務(wù)或操作,這篇文章主要為大家詳細(xì)介紹了go中是如何實現(xiàn)定時任務(wù)的,感興趣的可以了解下
    2023-11-11
  • 一站式解決方案:在Windows和Linux上快速搭建Go語言開發(fā)環(huán)境

    一站式解決方案:在Windows和Linux上快速搭建Go語言開發(fā)環(huán)境

    本文將介紹如何在Windows和Linux操作系統(tǒng)下搭建Go語言開發(fā)環(huán)境,以幫助您更高效地進(jìn)行Go語言開發(fā),需要的朋友可以參考下
    2023-10-10
  • Go 語言中的指針的使用

    Go 語言中的指針的使用

    在Go語言中,指針是存儲另一變量內(nèi)存地址的變量,通過&操作符獲取變量地址,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2024-09-09
  • Go語言編程通過dwarf獲取內(nèi)聯(lián)函數(shù)

    Go語言編程通過dwarf獲取內(nèi)聯(lián)函數(shù)

    這篇文章主要為大家介紹了Go語言編程通過dwarf獲取內(nèi)聯(lián)函數(shù)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-11-11
  • Golang使用gzip壓縮字符減少redis等存儲占用的實現(xiàn)

    Golang使用gzip壓縮字符減少redis等存儲占用的實現(xiàn)

    本文主要介紹了Golang使用gzip壓縮字符減少redis等存儲占用的實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-01-01
  • Golang中sync.Mutex的源碼分析

    Golang中sync.Mutex的源碼分析

    這篇文章將帶大家從源碼分析一下Golang中sync.Mutex的使用,文中的示例代碼講解詳細(xì),對我們學(xué)習(xí)Golang有一定的幫助,需要的可以參考一下
    2023-03-03

最新評論