偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

我所理解的 Go 的 GPM 模型

開發(fā) 前端 人工智能
理解 ??main?? 函數(shù)本身也是一個 goroutine 有助于更好地認識 Go 的并發(fā)模型的一致性:所有用戶代碼都運行在 goroutine 之上,由統(tǒng)一的 GPM 模型進行調(diào)度和管理。這體現(xiàn)了 Go 在語言層面和運行時層面將并發(fā)作為一等公民的設計哲學。

Go 語言(Golang)的一大顯著特性是在其語法層面就內(nèi)建了對協(xié)程,即 goroutine 的支持,并且其運行時(runtime)系統(tǒng)為這一功能提供了強大且原生的支撐。在我看來,選擇使用協(xié)程而非傳統(tǒng)的線程來支持高并發(fā)任務,帶來了諸多益處:

  • 切換成本更低 :協(xié)程的切換是純用戶態(tài)的操作,由 Go runtime 直接控制,避免了線程切換時需要在內(nèi)核態(tài)和用戶態(tài)之間傳遞上下文信息的開銷。相比之下,線程切換由操作系統(tǒng)(OS)層面實現(xiàn),成本更高。
  • 調(diào)度靈活性 :goroutine 的調(diào)度由 Go runtime 決定,而非操作系統(tǒng)。這使得 Go 可以根據(jù)應用特性實現(xiàn)更優(yōu)化的調(diào)度策略。
  • 支持大規(guī)模并發(fā) :由于協(xié)程在用戶態(tài)實現(xiàn)且資源占用?。ɡ?,初始棧空間通常只有幾 KB),因此可以輕松創(chuàng)建和管理成千上萬甚至數(shù)百萬的 goroutine,遠超傳統(tǒng)線程所能支持的并發(fā)量。
  • 創(chuàng)建與銷毀成本低 :goroutine 的創(chuàng)建和銷毀由 Go runtime 管理,其開銷遠小于操作系統(tǒng)線程。它們的??臻g是動態(tài)伸縮的,初始分配很小,按需增長,回收也更高效。
  • 簡化的并發(fā)編程模型 :通過 channel 和 select 等機制,goroutine 使得編寫和理解并發(fā)邏輯更為簡單和安全,減少了對傳統(tǒng)并發(fā)編程中復雜鎖機制的依賴。

然而,這些輕量級的 goroutine 終究需要依托實際的操作系統(tǒng)線程才能在 CPU 上執(zhí)行。Go 語言是如何高效管理這些 goroutine 的呢?這就引出了我們今天要深入探討的核心機制—— GPM 模型 。

總體談談 GPM

GPM 是 Go 調(diào)度器中三個核心組件的縮寫:

  • G (Goroutine) :即 Go 協(xié)程。它是 Go 程序中并發(fā)執(zhí)行的基本單元,擁有自己的??臻g、指令指針以及其他用于調(diào)度和執(zhí)行的上下文信息。G 的數(shù)量可以非常龐大。
  • P (Processor) :邏輯處理器。P 并非指物理 CPU 核心,而是 Go runtime 中的一個概念,它代表了 M (內(nèi)核線程) 執(zhí)行 Go 代碼所需要的上下文和資源,例如本地可運行 G 的隊列(Local Run Queue, LRQ)、內(nèi)存分配狀態(tài)等。每個 P 同時只能運行一個 G。P 的數(shù)量通常由環(huán)境變量 GOMAXPROCS 決定,默認情況下等于可用的 CPU 核心數(shù)。
  • M (Machine) :內(nèi)核線程,即操作系統(tǒng)管理的線程。M 是實際執(zhí)行 Go 代碼的實體。一個 M 必須與一個 P 關(guān)聯(lián)才能執(zhí)行 G。

我們首先從宏觀層面理解這種設計背后的考量:

通過設定 GOMAXPROCS 來控制 P 的數(shù)量,Go 程序既能確保充分利用多核 CPU 的計算能力,又避免了因過多線程競爭 CPU 資源而導致的性能下降。通常,P 的數(shù)量與 CPU 核心數(shù)相等,這意味著在理想情況下,每個核心都有一個 P 在積極地調(diào)度和執(zhí)行 G。

P 的角色至關(guān)重要,它作為 G 和 M 之間的橋梁。P 持有一個本地可運行 G 的隊列 (LRQ),當 M 需要執(zhí)行任務時,它會從其關(guān)聯(lián)的 P 的 LRQ 中獲取 G 來執(zhí)行。這種設計使得 G 的調(diào)度大部分發(fā)生在用戶態(tài),避免了頻繁的內(nèi)核態(tài)切換。

此外,P 與 M 的結(jié)合實現(xiàn)了線程的復用。當一個 M 因為執(zhí)行的 G 進行了阻塞性的系統(tǒng)調(diào)用(syscall)而被阻塞時,它所關(guān)聯(lián)的 P 可以被釋放,并被另一個空閑的 M 或者一個新創(chuàng)建的 M 獲取,從而繼續(xù)執(zhí)行 P 本地隊列中的其他 G。這樣就避免了因為少數(shù)阻塞操作導致大量線程閑置,同時也減少了線程頻繁創(chuàng)建和銷毀的開銷,相當于 Go runtime 內(nèi)部實現(xiàn)了一個高效的線程池。這一切對編寫 Go 代碼的用戶來說是透明的。

GPM 是如何調(diào)度的?

要理解 GPM 的調(diào)度機制,首先需要了解幾個關(guān)鍵的概念和數(shù)據(jù)結(jié)構(gòu):

  • 全局運行隊列 (Global Run Queue, GRQ) :當 P 的本地運行隊列沒有空間,或者某些 G(例如從網(wǎng)絡調(diào)用返回的 G、被搶占的 G)被喚醒或需要重新調(diào)度時,它們可能會被放入全局運行隊列。
  • P 的本地運行隊列 (Local Run Queue, LRQ) :每個 P 都有一個自己的 LRQ,用于存放待在該 P 上執(zhí)行的 G。M 會優(yōu)先從其關(guān)聯(lián) P 的 LRQ 中獲取 G。LRQ 的存在減少了對 GRQ 的競爭,提高了調(diào)度效率。
  • g0 :每個 M 都有一個特殊的 goroutine,稱為 g0。g0 擁有自己的棧空間(獨立于用戶 G 的棧,通常較大),主要用于執(zhí)行調(diào)度相關(guān)的代碼、垃圾回收的輔助工作以及其他運行時任務。當 M 需要切換到某個用戶 G 執(zhí)行時,會從 g0 棧切換到用戶 G 的棧;反之亦然。
  • m.curg :指向當前在 M 上運行的用戶 G。
  • G 的狀態(tài) :Goroutine 在其生命周期中會經(jīng)歷多種狀態(tài),例如 _Gidle(閑置,剛被分配還未使用)、_Grunnable(可運行,在運行隊列中等待調(diào)度)、_Grunning(運行中,正在 M 上執(zhí)行)、_Gsyscall(進行系統(tǒng)調(diào)用,M 已與 P 分離)、_Gwaiting(等待中,如等待 channel 操作、鎖、或定時器)、_Gdead(已結(jié)束,資源可回收)、_Gcopystack(棧復制中,通常在棧增長時發(fā)生)、_Gpreempted(被搶占,等待重新調(diào)度)。
  • P 的狀態(tài) :P 也有不同的狀態(tài),如 _Pidle(閑置,沒有 M 與之關(guān)聯(lián)或沒有可運行的 G)、_Prunning(運行中,有 M 與之關(guān)聯(lián)并正在執(zhí)行 G 或調(diào)度代碼)、_Psyscall(其關(guān)聯(lián)的 M 正在進行一個阻塞的系統(tǒng)調(diào)用,P 本身可能被其他 M 使用)、_Pgcstop(因垃圾回收而停止)、_Pdead(不再使用,例如 GOMAXPROCS 被調(diào)小時)。

調(diào)度決策在很大程度上是每個 M 各自獨立在其 g0 棧上執(zhí)行的。當一個 M 空閑下來(例如,其當前 G 執(zhí)行完畢或被阻塞),它會運行調(diào)度代碼來尋找下一個可運行的 G。

  • 沒有單一的“總控”M :Go 的調(diào)度器設計上是去中心化的,沒有一個特定的 M 作為“總控制器”來指揮所有其他 M。這種設計避免了單點瓶頸,提高了并發(fā)度。
  • 協(xié)調(diào)機制 :盡管調(diào)度是分布式的,但 M 之間通過一些共享結(jié)構(gòu)和機制進行協(xié)調(diào):

a.全局運行隊列 (GRQ) :為所有 P 提供了一個共享的 G 來源。

b.工作竊取 (Work Stealing) :空閑的 M 會嘗試從其他 P 的 LRQ 中“竊取”任務。

c.sysmon 后臺監(jiān)控線程 :這是一個特殊的 M(不與 P 綁定),它負責一些全局性的協(xié)調(diào)任務,比如垃圾回收的觸發(fā)和輔助、網(wǎng)絡輪詢器(Netpoller)事件的處理(間接影響調(diào)度,通過將等待 I/O 的 G 變?yōu)榭蛇\行狀態(tài))、以及檢測并搶占長時間運行的 G。sysmon 更像是一個維護者和協(xié)調(diào)者,而非一個命令下發(fā)者。

d.P 的管理 :Go runtime 負責管理 P 的池。當 M 因系統(tǒng)調(diào)用阻塞時釋放 P,或當有空閑 P 和可運行 G 時,runtime 會嘗試喚醒或創(chuàng)建 M 來綁定這些 P。

  • 這種分布式的調(diào)度配合全局協(xié)調(diào)機制,使得 Go 的調(diào)度器既高效又具有良好的伸縮性。

接下來,我們通過幾個例子來具體闡述調(diào)度過程:

1. 基本調(diào)度流程

假設我們有一個 P0 和一個 M0,P0 的 LRQ 中有 G1。

  • 獲取 G :M0 啟動后,或者當它完成了前一個 G 的執(zhí)行后,它會首先查看其關(guān)聯(lián)的 P0 的 LRQ。此時,M0 在 g0 棧上執(zhí)行調(diào)度邏輯。
  • 執(zhí)行 G :M0 從 P0 的 LRQ 中取出 G1。G1 的狀態(tài)從 _Grunnable 變?yōu)?nbsp;_Grunning,P0 的狀態(tài)保持或變?yōu)?nbsp;_Prunning。M0 的 m.curg 指向 G1。隨后,M0 從 g0 棧切換到 G1 的棧,開始執(zhí)行 G1 的代碼。
  • G 執(zhí)行完畢 :當 G1 執(zhí)行完畢(例如函數(shù)返回),它會切換回 g0 棧。G1 的狀態(tài)變?yōu)?nbsp;_Gdead,其資源會被回收。M0 (在 g0 棧上) 接著會再次嘗試從 P0 的 LRQ 尋找下一個可運行的 G。
  • LRQ 為空 :如果 P0 的 LRQ 為空,M0(在 g0 棧上)會嘗試進行 工作竊取 (work-stealing) ,它會隨機查看其他 P 的 LRQ,如果發(fā)現(xiàn)有 G,就竊取一半過來放到自己的 P0 的 LRQ 中。如果其他 P 的 LRQ 也都為空,M0 會嘗試從 GRQ 獲取 G。
  • GRQ 也為空 :如果 GRQ 也為空,M0 可能會將 P0 置為 _Pidle 狀態(tài),并解除 M0 與 P0 的關(guān)聯(lián),M0 自身也可能進入休眠(park)狀態(tài),等待新的 G 到來時被喚醒?;蛘撸琈0 會去自旋(spinning)一段時間,期望短期內(nèi)有新的 G 產(chǎn)生。

自旋 (spinning) 是指 M 在一個緊密的循環(huán)中不斷檢查是否有可運行的 G,而不立即放棄 CPU。

  • CPU 占用 :在自旋期間,M 會持續(xù)消耗 CPU 資源,如果該 CPU 核心上沒有其他更高優(yōu)先級的任務,它可能會達到 100% 的占用率。
  • 為何自旋 :這是一種以 CPU 時間換取調(diào)度延遲的策略。如果新的 G 很快就能變?yōu)榭蛇\行狀態(tài)(例如,另一個 M 正在處理一個即將完成的短任務,或者一個 I/O 事件即將觸發(fā)),那么自旋可以避免 M 進入休眠和隨后被喚醒所帶來的開銷(這通常涉及操作系統(tǒng)層面的上下文切換,成本相對較高)。
  • 自旋的條件與限制 :Go runtime 中的自旋不是無限制的。

a.通常,只有當系統(tǒng)中存在其他活躍的 P(意味著其他 M 正在工作,有可能產(chǎn)生新的 G)時,M 才會進入自旋狀態(tài)。如果所有 P 都已空閑,則 M 傾向于直接休眠。

b.同時,runtime 會限制并發(fā)自旋的 M 的數(shù)量,以避免過多的 M 同時無效自旋。

c.自旋的持續(xù)時間或迭代次數(shù)是有限的。如果經(jīng)過短暫的自旋后仍未找到 G,M 將停止自旋,釋放其 P(如果 P 上確實沒有 G),并進入休眠(park)狀態(tài),將 CPU 讓給其他進程或線程。

自旋是一種短期內(nèi)積極尋找任務的優(yōu)化手段,適用于預期任務會很快出現(xiàn)的場景,以減少調(diào)度開銷,但它確實會短暫地增加 CPU 使用率。

在這個過程中,P 的狀態(tài)也會相應變化。例如,當一個 M 成功與一個 P 綁定并開始查找或執(zhí)行 G 時,P 的狀態(tài)會是 _Prunning。如果 P 的 LRQ 和 GRQ 都長時間為空,并且沒有 M 依附于它,它可能進入 _Pidle 狀態(tài)。

G 的棧數(shù)據(jù)切換發(fā)生在 M 從 g0 棧切換到用戶 G 的棧,以及從用戶 G 的棧切回 g0 棧時。這個切換操作會保存和恢復各自的棧指針和寄存器等上下文信息。

2. 棧的伸縮與 P 的競爭

  • 棧的動態(tài)伸縮 :Goroutine 的棧在創(chuàng)建時通常較?。ɡ?2KB)。當 G 執(zhí)行的函數(shù)調(diào)用深度增加,需要的??臻g超過當前大小時,Go runtime 會觸發(fā)一個稱為 morestack 的機制。該機制會分配一個新的、更大的棧段,并將舊棧的內(nèi)容拷貝到新棧段,然后 G 繼續(xù)在新棧上執(zhí)行。這個過程對用戶是透明的。當函數(shù)返回,棧使用量減少時,雖然不會立即縮小,但在垃圾回收期間,如果發(fā)現(xiàn)棧使用率過低,可能會進行棧的收縮(shrinkstack)。
  • P 的競爭 :在 Go 程序啟動時,會根據(jù) GOMAXPROCS 創(chuàng)建相應數(shù)量的 P。如果 M 的數(shù)量少于 P 的數(shù)量(例如,某些 M 因為系統(tǒng)調(diào)用阻塞了),或者有空閑的 P 和待運行的 G,運行時可能會喚醒或創(chuàng)建新的 M 來綁定這些 P。一個 M 必須獲取到一個 P 才能運行 Go 代碼。如果所有 P 都在 _Prunning 狀態(tài)(即都有 M 在其上運行 G),那么新創(chuàng)建的 G 只能進入 LRQ 或 GRQ 等待。當一個 M 從阻塞的系統(tǒng)調(diào)用返回,或者一個 G 執(zhí)行完畢,它會嘗試獲取一個 P 來繼續(xù)執(zhí)行。

3. I/O 操作與網(wǎng)絡調(diào)度

當一個 G (假設為 Gx,在 M1/P1 上運行) 發(fā)起一個阻塞性的 I/O 操作,比如網(wǎng)絡讀寫時,情況會變得特殊:

  • 進入系統(tǒng)調(diào)用 :Gx 在 M1 上調(diào)用了一個阻塞的 read。Go runtime 的 syscall 包中的函數(shù)通常會進行特殊處理。M1 會即將進入阻塞狀態(tài)。
  • 釋放 P :為了不讓 P1 上的其他 G 被餓死,M1 會釋放 P1。P1 此時 通常會連同其 LRQ 中的 G 一起 ,被移交給一個其他可用的、處于空閑狀態(tài)的 M (例如 M2,可以是已存在的空閑 M,或者是 runtime 根據(jù)需要新創(chuàng)建的 M 來接管這個 P)。M1 則帶著 Gx 進入阻塞的系統(tǒng)調(diào)用。Gx 的狀態(tài)變?yōu)?nbsp;_Gsyscall

這里需要考慮到調(diào)度器內(nèi)部實現(xiàn)的復雜性和一些邊緣情況。核心原則是: LRQ 始終與 P 綁定 。當 M1 因 Gx 的系統(tǒng)調(diào)用而將要阻塞時,它會釋放 P1。

  • 主要情況 :調(diào)度器會立即嘗試尋找一個空閑的 M (M2) 來接管 P1。如果找到,M2 就綁定 P1,并開始執(zhí)行 P1 的 LRQ 中的 G。這時,P1 及其 LRQ 完整地從 M1 轉(zhuǎn)移到了 M2。
  • 沒有立即可用的 M:如果暫時沒有空閑的 M 可以立即接管 P1,P1 會被放入一個空閑 P 隊列 (pidle 列表)。其 LRQ 中的 G 仍然與 P1 綁定并處于 _Grunnable 狀態(tài)。一旦有 M 可用(例如 M1 從系統(tǒng)調(diào)用返回后變?yōu)榭臻e,或者 sysmon 檢測到需要更多 M 并創(chuàng)建/喚醒了一個),這個 M 就會從 pidle 列表中獲取 P1,并開始執(zhí)行其 LRQ 中的 G。
  • 因此,P1 的 LRQ 中的 G 總是和 P1 在一起 。關(guān)鍵在于 P1 由哪個 M 來服務。如果 M1 阻塞了,它就不能服務 P1,所以 P1 必須尋找新的 M,或者等待 M 變?yōu)榭捎谩?/span>
  • 網(wǎng)絡輪詢器 (Netpoller) :Go runtime 內(nèi)部維護了一個網(wǎng)絡輪詢器(在 Linux 上通常基于 epoll,在 macOS 上基于 kqueue,在 Windows 上基于 iocp)。當 Gx 發(fā)起網(wǎng)絡 I/O 時,其對應的文件描述符會被注冊到這個網(wǎng)絡輪詢器中。M1 線程本身會阻塞在系統(tǒng)調(diào)用上(或者對于非阻塞 I/O,G 會等待 netpoller 的通知),但它不再持有 P。
  • I/O 就緒與喚醒 :當網(wǎng)絡輪詢器檢測到 Gx 等待的文件描述符上的 I/O 操作就緒(例如數(shù)據(jù)可讀),它會通知調(diào)度器。Gx 會被標記為 _Grunnable,并被放回到某個 P 的 LRQ (可能是原來的 P1,如果它恰好空閑) 或者 GRQ 中。

Go 的標準庫網(wǎng)絡操作在底層通常被封裝為非阻塞模式,并與 netpoller 集成。

  • 注冊與等待 :當 Gx 調(diào)用如 net.Conn.Read() 時,如果數(shù)據(jù)尚未到達,runtime 不會真的讓 M1 線程阻塞在內(nèi)核的 read() 調(diào)用上。相反,它會將 Gx 的狀態(tài)置為 _Gwaiting,并將與該連接對應的文件描述符 (FD) 注冊到 netpoller 中,請求 netpoller 在該 FD 可讀時通知。然后,M1 釋放 P1(或 P1 被其他 M 接管),M1 可以去執(zhí)行其他 G 或者休眠。
  • Netpoller 的監(jiān)控 :Netpoller (通常是一個獨立的系統(tǒng)線程或由 sysmon 驅(qū)動) 使用操作系統(tǒng)提供的事件通知機制 (如 epoll_waitkevent 等) 來同時監(jiān)控大量已注冊的 FD。這些機制允許一個線程高效地等待多個 FD 上的事件,而無需為每個 FD 單獨創(chuàng)建一個線程。
  • 事件通知 :當操作系統(tǒng)內(nèi)核檢測到某個 FD 上的數(shù)據(jù)已到達(對于 read 操作)或可以發(fā)送數(shù)據(jù)(對于 write 操作)時,它會通知 netpoller。
  • G 的喚醒 :Netpoller 收到內(nèi)核通知后,會識別出是哪個 G 在等待這個 FD 上的事件。它會將該 G 從 _Gwaiting 狀態(tài)轉(zhuǎn)換回 _Grunnable 狀態(tài),并將其放入一個運行隊列 (通常是 GRQ,有時也可能是某個 P 的 LRQ,例如上次運行該 G 的 P,以期利用緩存局部性)。
  • 調(diào)度執(zhí)行實際讀操作 :一旦 Gx 變?yōu)?nbsp;_Grunnable,它就和其他等待調(diào)度的 G 一樣。當某個 M/P 組合選中它執(zhí)行時,它會從之前中斷的地方恢復。此時,由于 netpoller 已經(jīng)確認數(shù)據(jù)就緒,G 可以執(zhí)行實際的、現(xiàn)在不會阻塞的 read() 操作來獲取數(shù)據(jù)。
  • 重新調(diào)度執(zhí)行 :一旦 Gx 變?yōu)?nbsp;_Grunnable,它就和其他可運行的 G 一樣,等待某個 M/P 組合來執(zhí)行它。當輪到它時,它會從上次阻塞的地方繼續(xù)執(zhí)行。

這種機制確保了少數(shù) G 的阻塞性 I/O 不會阻塞整個程序的并發(fā)執(zhí)行。M 的數(shù)量可能會根據(jù)需要動態(tài)調(diào)整(在一定范圍內(nèi)),以適應負載情況。

創(chuàng)建一個 go func(){}() 發(fā)生了什么?

當你執(zhí)行一行代碼 go func(){ ... }() 時,Go runtime 會執(zhí)行以下步驟:

  • 創(chuàng)建 G 對象 :首先,runtime 會在堆上分配并初始化一個新的 G 對象。這個對象包含了新 goroutine 的棧信息(初始分配一個小棧)、程序計數(shù)器(指向匿名函數(shù)的起始位置)以及其他狀態(tài)信息。

a.設置初始狀態(tài) :新創(chuàng)建的 G 的初始狀態(tài)被設置為 _Grunnable,表示它已經(jīng)準備好運行,只等待調(diào)度器的調(diào)度。

b.放入隊列 :這個新的 _Grunnable 的 G 通常會被嘗試放入當前 M 所關(guān)聯(lián)的 P 的 LRQ。

  • 如果該 P 的 LRQ 已滿,runtime 會嘗試將 P 的 LRQ 中的一部分 G(包括這個新的 G)均衡到 GRQ 中。
  • 在某些情況下,如果創(chuàng)建 G 的 P 處于特殊狀態(tài),或者為了更好的負載均衡,新的 G 也可能直接被放入 GRQ。
  • 創(chuàng)建 G 的函數(shù)返回 :go 語句本身是一個非阻塞調(diào)用。執(zhí)行 go 語句的 goroutine 會繼續(xù)執(zhí)行其后續(xù)代碼,而不會等待新創(chuàng)建的 goroutine 開始或完成執(zhí)行。
  • 調(diào)度與執(zhí)行 :新創(chuàng)建的 G 現(xiàn)在位于某個運行隊列中。當某個 M(可能就是當前的 M,也可能是其他 M)在未來的某個調(diào)度點(例如,當前 G 執(zhí)行完畢、發(fā)生搶占、或 M 從系統(tǒng)調(diào)用返回時)查找可運行的 G 時,它就有機會從 LRQ 或 GRQ 中獲取這個新的 G。獲取到 G 后,M 會設置好運行環(huán)境(切換到該 G 的棧,設置 G 的狀態(tài)為 _Grunning 等),然后開始執(zhí)行該匿名函數(shù)內(nèi)的代碼。

整個過程與上面描述的 GPM 調(diào)度機制緊密相連,新的 G 只是作為調(diào)度器可調(diào)度的一個單元被高效地管理起來。

調(diào)度策略與搶占機制

Go 的調(diào)度器采用了一些關(guān)鍵策略來保證公平性和效率:

  • 工作竊取 (Work Stealing) :如前所述,當一個 P 的 LRQ 為空時,其關(guān)聯(lián)的 M 會嘗試從其他 P 的 LRQ 中“竊取”一半的 G 到自己的 LRQ,或者從 GRQ 中獲取 G。這有助于在 P 之間均勻分配工作負載,防止某些 P 空閑而另一些 P 過載。
  • 搶占 (Preemption) :在 Go 的早期版本中(1.14 之前),搶占主要是協(xié)作式的。也就是說,一個 goroutine 主動放棄 CPU 的執(zhí)行權(quán)通常發(fā)生在函數(shù)調(diào)用時(編譯器會在函數(shù)入口處插入檢查點,判斷是否需要進行棧增長以及是否需要被搶占)、channel 操作、select 語句、以及一些同步原語的調(diào)用點。這意味著如果一個 goroutine 執(zhí)行一個沒有任何函數(shù)調(diào)用的密集計算循環(huán) (for {}),它可能會長時間占據(jù) M,導致同一個 P 上的其他 goroutine 餓死。

從 Go 1.14 版本開始,引入了 基于信號的異步搶占機制 (asynchronous preemption) ,以解決上述問題:

  • sysmon 后臺監(jiān)控線程 :Go runtime 有一個名為 sysmon 的特殊 M(不關(guān)聯(lián) P),它會定期進行一些維護工作,其中就包括檢查是否有 G 運行時間過長(例如,超過一個固定的時間片,通常是 10ms)。
  • 發(fā)送信號 :如果 sysmon 發(fā)現(xiàn)某個 G 在一個 M 上運行時間過長,它會向該 M 發(fā)送一個搶占信號(例如,在 Unix 系統(tǒng)上是 SIGURG)。
  • 信號處理 :M 接收到信號后,會中斷當前正在執(zhí)行的 G。G 的上下文(主要是寄存器)會被保存,其狀態(tài)會被標記為 _Gpreempted 或類似狀態(tài),然后被放回到運行隊列(通常是 GRQ,以給其他 P 機會執(zhí)行它,避免立即在同一個 P 上再次調(diào)度)。
  • 重新調(diào)度 :M 隨后會進入調(diào)度循環(huán)(在其 g0 棧上),選擇下一個可運行的 G 來執(zhí)行。

這種異步搶占機制確保了即使是那些沒有主動讓出 CPU 的計算密集型 goroutine 也能夠被公平地調(diào)度,從而提高了整個系統(tǒng)的響應性和并發(fā)任務的并行度。它使得調(diào)度器更加健壯,不易受到不良編寫的 goroutine 的影響。

func main 也是一個 goroutine

當一個 Go 程序啟動時,main 包下的 main 函數(shù)并不是直接在某個原始線程上執(zhí)行,而是由 Go runtime 創(chuàng)建的第一個用戶級 goroutine,通常被稱為 main goroutine 。

  • 初始化過程 :Go 程序的入口點實際上是 runtime 的一段引導代碼。這段代碼會負責初始化調(diào)度器、垃圾回收器、創(chuàng)建必要的 M 和 P,然后創(chuàng)建一個 G 來執(zhí)行用戶編寫的 main.main() 函數(shù)。
  • 與其他 goroutine 平等 :這個 main goroutine 在行為上與用戶通過 go 關(guān)鍵字創(chuàng)建的其他 goroutine 是平等的。它也擁有自己的棧,受 GPM 調(diào)度器的管理,可以被搶占,也可以創(chuàng)建新的 goroutine。
  • 程序生命周期 :main goroutine 的結(jié)束標志著整個程序的結(jié)束。當 main 函數(shù)返回時,Go runtime 會開始關(guān)閉程序。此時,所有其他仍在運行的 goroutine 都會被強制終止,除非程序使用了像 sync.WaitGroup 這樣的機制來顯式等待其他 goroutine 完成。
  • 退出碼 :main 函數(shù)沒有返回值。程序如果正常退出,通常退出碼為 0。如果發(fā)生 panic 且未被 recover,或者調(diào)用了 os.Exit(code),則會以相應的狀態(tài)退出。

理解 main 函數(shù)本身也是一個 goroutine 有助于更好地認識 Go 的并發(fā)模型的一致性:所有用戶代碼都運行在 goroutine 之上,由統(tǒng)一的 GPM 模型進行調(diào)度和管理。這體現(xiàn)了 Go 在語言層面和運行時層面將并發(fā)作為一等公民的設計哲學。

責任編輯:姜華 來源: Piper蛋窩
相關(guān)推薦

2025-06-03 02:00:00

2025-06-09 01:15:00

2025-05-28 03:00:00

2024-01-22 10:18:32

平臺工程開發(fā)人員技術(shù)

2019-10-08 10:37:46

設計技術(shù)程序員

2016-11-29 16:46:17

存儲閃存經(jīng)濟學

2015-11-09 10:12:08

大數(shù)據(jù)個性化推薦

2013-07-11 10:37:20

Java內(nèi)存模型

2009-08-07 14:09:47

垃圾郵件企業(yè)郵件安全梭子魚

2023-03-03 15:37:32

GMP 模型goroutine

2021-02-22 09:30:09

go開發(fā)環(huán)境桌面系統(tǒng)

2018-07-10 08:56:19

編程程序員開發(fā)

2024-12-03 15:15:22

2019-12-26 09:15:44

網(wǎng)絡IOLinux

2022-04-30 18:42:38

Go編程語言

2021-03-28 20:58:25

Go語言線程

2022-07-06 08:30:36

vuereactvdom

2015-09-02 09:02:21

阿里無線前端架構(gòu)

2017-05-24 10:12:54

前端FlexboxCSS3

2020-02-27 21:03:30

調(diào)度器架構(gòu)效率
點贊
收藏

51CTO技術(shù)棧公眾號