深入理解goroutine調(diào)度 2023-01-03

內(nèi)核態(tài)vs用戶態(tài)

操作系統(tǒng)運行時使用的ram存儲資源叫內(nèi)核態(tài)
用戶(上層軟件)運行時使用的ram存儲資源叫用戶態(tài)

對于32位系統(tǒng)而言,其最大尋址空間為 2^32次方=4G內(nèi)存。 linux系統(tǒng)內(nèi)核態(tài)和用戶態(tài)占比1:3,將最高的1G字節(jié)(從虛擬地址0xC0000000到0xFFFFFFFF),供內(nèi)核使用,稱為內(nèi)核空間,而將較低的3G字節(jié)(從虛擬地址0x00000000到0xBFFFFFFF),供各個進程使用,稱為用戶空間。 windows系統(tǒng)內(nèi)核態(tài)和用戶態(tài)占比2:2

程序大多數(shù)時間是運行在用戶態(tài),當(dāng)程序需要操作系統(tǒng)完成協(xié)助時會從用戶態(tài)切換到內(nèi)核態(tài)。
需要協(xié)助的場景有:

  • 系統(tǒng)調(diào)用:讀取文件、網(wǎng)卡發(fā)起網(wǎng)絡(luò)請求
  • 異常:缺頁、除以0等觸發(fā)異常
  • 中斷處理:接收網(wǎng)卡數(shù)據(jù)等

鑒于系統(tǒng)用戶態(tài)與內(nèi)核態(tài)的存在,線程實現(xiàn)方式分為內(nèi)核態(tài)線程和用戶態(tài)線程:

  • 內(nèi)核態(tài)線程: 操作系統(tǒng)層面實現(xiàn)的線程
  • 用戶態(tài)線程:在操作系統(tǒng)上層應(yīng)用實現(xiàn)的線程庫。但無論如何,用戶態(tài)線程如果想執(zhí)行(操作系統(tǒng)硬件資源),都需要映射或者綁定到內(nèi)核態(tài)線程

協(xié)程調(diào)度器模型演進

如何實現(xiàn)一個協(xié)程調(diào)度器?以下是協(xié)程調(diào)度器的模型演進

  • 1:1模型。無需用戶實現(xiàn)協(xié)程調(diào)度器,用戶態(tài)的協(xié)程和內(nèi)核線程一對一綁定,調(diào)度交由內(nèi)核完成
  • GM模型(普通m:n模型)。將內(nèi)核線程進行池化,用戶態(tài)協(xié)程加入一個全局隊列,內(nèi)核線程從全局隊列中取出用戶態(tài)協(xié)程并綁定(取出動作需要加全局鎖)
  • PM模型。GM模型全局隊列加全局鎖的操作過重,那么分而治之。我們引入一個結(jié)構(gòu)作為中間介質(zhì),這個結(jié)構(gòu)就是Proc。每個內(nèi)核線程和一個Proc是一一對應(yīng)關(guān)系,而Proc維護了一個私有的用戶態(tài)協(xié)程隊列。這樣一來,每個內(nèi)核線程都有私有的用戶態(tài)協(xié)程隊列。但問題又來了,私有的協(xié)程隊列設(shè)置多大合適?太小的話,隊列被占滿了怎么辦?太大的話,又會浪費。所以隊列需要動態(tài)增減?
  • GPM模型。在PM模型的基礎(chǔ)上,設(shè)置一個全局隊列。每次構(gòu)建G先嘗試放入P隊列,如果P隊列容量不足,則放入全局隊列。隊列是一個固定大小的數(shù)組,而全局隊列是一個鏈表,可以無限擴容

GPM模型下G的調(diào)度

GPM模型下,調(diào)度器如何進行G的調(diào)度?如果內(nèi)核態(tài)線程M阻塞(如socket請求)會阻塞相應(yīng)P的所有G執(zhí)行么?

M一旦進入系統(tǒng)調(diào)用后,會脫離go runtime的控制(此時控制權(quán)在操作系統(tǒng)內(nèi)核)。萬一系統(tǒng)調(diào)用阻塞,此時又無法進行搶占,整個M也就罷工了,M關(guān)聯(lián)的PG就無法被執(zhí)行。所以為了維持整個調(diào)度體系的高效運轉(zhuǎn),必然要在進入系統(tǒng)調(diào)用之前要做點什么以防患未然:

  • 非阻塞的系統(tǒng)調(diào)用, G 會和MP分離(以進行network call為例,G會從M上移走并掛到netpoller)

    G1 makes a network syscall

    G1 is moved to NetPoller

    G1 is back to LRQ of P

  • 阻塞的系統(tǒng)調(diào)用, MG 會和P分離(P另尋M),當(dāng)M從系統(tǒng)調(diào)用返回時,不會繼續(xù)執(zhí)行,而是將G放到run queue

    G1 is going to make a blocking syscall



    可以看到,一次阻塞的系統(tǒng)調(diào)用,必然導(dǎo)致一個M被獨占,這依然是耗費系統(tǒng)資源的事情

因此,Go 適合 IO 密集型的場景并不準確。更準確的是 Go 適合的是非阻塞式的IO 密集型的場景(如網(wǎng)絡(luò) IO 密集型場景,而非磁盤 IO 密集型)。甚至可以說,Go 對于磁盤 IO 密集型并不友好。
根本原因:網(wǎng)絡(luò) socket 句柄和文件句柄的不同。網(wǎng)絡(luò) IO 能夠用異步非阻塞的事件驅(qū)動的方式來管理,磁盤 IO 則不行,只能是同步阻塞的方式。

socket 句柄可讀可寫事件都有意義,socket buffer 里有數(shù)據(jù),說明對端網(wǎng)絡(luò)發(fā)數(shù)據(jù)過來了,即滿足可讀事件。有 buffer 可以寫,那么說明還能發(fā)送數(shù)據(jù),滿足可寫事件。socket 句柄可以設(shè)置為 noblocking (非阻塞的方式),這樣當(dāng)網(wǎng)絡(luò) IO 還未就緒的時候(比如read 一下沒有數(shù)據(jù))就可以在 Go 代碼里把調(diào)度權(quán)切走,去執(zhí)行其他協(xié)程,這樣就實現(xiàn)了網(wǎng)絡(luò) IO 的并發(fā)。

文件句柄可讀可寫事件則沒有意義,因為文件句柄理論上是永遠都是可讀可寫的,文件 IO 的 read/write 都是同步的 IO(比如read 一下,沒數(shù)據(jù)直接就卡住了),所以磁盤 IO 的完成只能同步等待。然而磁盤 IO 的等待則會帶來 Go 最不能容忍的事情:卡線程。
Go 的代碼執(zhí)行者是系統(tǒng)線程,也就是 G-M-P 模型的 M ,M 不斷的從隊列 P 中取 G(協(xié)程任務(wù))出來執(zhí)行。當(dāng) G 出現(xiàn)等待事件的時候(比如網(wǎng)絡(luò) IO),那么立馬切走,取下一個執(zhí)行。這樣讓 M 一直不停的滿載,就能保證 Go 協(xié)程任務(wù)的高吞吐。那么問題來了,如果某個 G 卡線程了,就相當(dāng)于這個 M 被廢了,吞吐能力就下降。如果 M 全卡住了那相當(dāng)于整個程序卡死了。然而對于類似系統(tǒng)調(diào)用這種卡線程卻是無法人為控制的。Go runtime 為了解決這個問題,就只能創(chuàng)建更多的線程來保證一直有可運行的 M 。所以,你經(jīng)常會發(fā)現(xiàn),當(dāng)系統(tǒng)調(diào)用很慢的時候,M 的數(shù)量會變多,甚至?xí)q

參考:
https://qiankunli.github.io/2020/11/21/goroutine_system_call.html

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容