首頁 後端開發 Golang Go 為什麼這麼'快”

Go 為什麼這麼'快”

Mar 04, 2020 am 09:34 AM
go

本文主要介紹了 Go 程式為了實現極高的並發效能,其內部調度器的實作架構(G-P-M 模型),以及為了最大限度地利用運算資源,Go 調度器是如何處理執行緒阻塞的場景。

怎麼讓我們的系統更快

隨著資訊科技的快速發展,單一伺服器處理能力越來越強,迫使程式設計模式由從前的串行模式升級到並發模型。

並發模型包含IO 多路復用、多進程以及多線程,這幾種模型都各有優劣,現代複雜的高並發架構大多是幾種模型協同使用,不同場景應用不同模型,揚長避短,發揮伺服器的最大效能。

而多線程,因為其輕量且易用,成為並發程式設計中使用頻率最高的並發模型,包括後衍生的協程等其他子產品,也都基於它。

並發 ≠ 並行

並發 (concurrency) 和 並行 ( parallelism) 是不同的。

在單一  CPU  核上,執行緒透過時間片或讓出控制權來實現任務切換,達到  "同時"  執行多個任務的目的,這就是所謂的並發。但實際上任何時刻都只有一個任務被執行,其他任務則透過某種演算法來排隊。

多核心  CPU  可以讓同一進程內的  "多個執行緒"  做到真正意義上的同時運行,這才是並行。

進程、執行緒、協程

進程:進程是系統進行資源分配的基本單位,有獨立的記憶體空間。

執行緒:執行緒是 CPU 調度和分派的基本單位,執行緒依附於行程存在,每個執行緒會共享父行程的資源。

協程:協程是一種用戶態的輕量級線程,協程的調度完全由使用者控制,協程間切換只需要保存任務的上下文,沒有核心的開銷。

線程上下文切換

由於中斷處理,多任務處理,用戶態切換等原因會導致CPU 從一個線程切換到另一個線程,切換過程需要保存當前進程的狀態並恢復另一個進程的狀態。

上下文切換的代價是高昂的,因為在核心上交換執行緒會花費很多時間。上下文切換的延遲取決於不同的因素,大概在  50  到  100  納秒之間。考慮到硬體平均在每個核心上每納秒執行  12  條指令,那麼一次上下文切換可能會花費  600  到  1200  條指令的延遲時間。實際上,上下文切換佔用了大量程式執行指令的時間。

如果存在跨核上下文切換(Cross-Core Context Switch),可能會導致CPU 快取失敗(CPU 從快取存取資料的成本大約 3  到 40  個時鐘週期,並從主記憶體存取資料的成本大約100  到 300  個時鐘週期),這種場景的切換成本會更昂貴。

Golang 為並發而生

Golang 從 2009 年正式發布以來,依靠其極高運行速度和高效的開發效率,迅速佔據市場份額。 Golang 從語言層級支援並發,透過輕量級協程 Goroutine 來實現程式並發運行。

Goroutine 非常輕量,主要體現在以下兩個方面:

上下文切換代價小: Goroutine 上下文切換只涉及三個暫存器(PC / SP / DX)的值修改;而對比線程的上下文切換則需要涉及模式切換(從用戶態切換到內核狀態)、以及16 個寄存器、PC、SP…等寄存器的刷新;

內存佔用少:線程棧空間通常是2M,Goroutine 堆疊空間最小2K;

Golang 程式中可以輕鬆支援10w 等級的Goroutine 運行,而執行緒數量達到1k 時,記憶體佔用就已經達到2G。

Go 調度器實作機制:

Go 程式透過調度器來調度Goroutine 在核心執行緒上執行,但是Goroutine 並不會直接綁定OS 執行緒M - Machine運行,而是由Goroutine Scheduler 中的 P - Processor (邏輯處理器)來取得核心執行緒資源的『中介』。

Go 調度器模型我們通常叫做G-P-M 模型,他包含4 個重要結構,分別是G、P、M、Sched:

G:Goroutine,每個Goroutine 對應一個G 結構體,G 儲存Goroutine 的運行堆疊、狀態以及任務函數,可重複使用。

G 並非執行體,每個 G 需要綁定到 P 才能被調度執行。

P: Processor,表示邏輯處理器,對 G 來說,P 相當於 CPU 核,G 只有綁定到 P 才能被調度。對 M 來說,P 提供了相關的執行環境(Context),如記憶體分配狀態(mcache),任務佇列(G)等。

P 的數量決定了系統內最大可平行的 G 的數量(前提:實體 CPU 核數  >= P 的數量)。

P 的數量由使用者設定的 GoMAXPROCS 決定,但不論 GoMAXPROCS 設定為多大,P 的數量最大為 256。

M: Machine,OS 核心執行緒抽象,代表真正執行運算的資源,在綁定有效的P 後,進入schedule 迴圈;而schedule 迴圈的機制大致上是從Global 佇列、P 的Local 佇列以及wait 佇列中獲取。

M 的數量是不定的,由 Go Runtime 調整,為了防止創建過多 OS 執行緒導致系統調度不過來,目前預設最大限制為 10000 個。

M 不保留 G 狀態,這是 G 可以跨 M 調度的基礎。

Sched:Go 調度器,它維護有儲存 M 和 G 的佇列以及調度器的一些狀態資訊等。

調度器循環的機制大致是從各種隊列、P 的本地隊列中獲取G,切換到G 的執行棧上並執行G 的函數,調用Goexit 做清理工作並回到M,如此反覆。

理解M、P、G 三者的關係,可以透過經典的地鼠推車搬磚的模型來說明其三者關係:

Go 為什麼這麼快”

地鼠(Gopher)的工作任務是:工地上有若干磚頭,地鼠借助小車把磚頭運送到火種上去燒製。 M 就可以看作圖中的地鼠,P 就是小車,G 就是小車裡裝的磚。

弄清楚了它們三者的關係,下面我們就開始重點聊地鼠是如何在搬運磚塊的。

Processor(P):

根據使用者設定的  GoMAXPROCS 值建立一批小車(P)。

Goroutine(G):

透過Go 關鍵字就是用來創建一個 Goroutine,也相當於製造一塊磚(G),然後將這塊磚(G)放入當前這輛小車(P)中。

Machine (M):

地鼠(M)不能透過外部創建出來,只能磚(G)太多了,地鼠(M)又太少了,實在忙不過來,剛好還有空閒的小車(P)沒有使用,那就從別處再藉些地鼠(M)過來直到把小車(P)用完為止。

這裡有一個地鼠(M)不夠用,從別處借地鼠(M)的過程,這個過程就是創建一個核心線程(M)。

要注意的是:地鼠(M)  如果沒有小車(P)是沒辦法運磚的,小車(P)的數量決定了能夠幹活的地鼠(M)數量,在Go程式裡面對應的是活動執行緒數;

在Go 程式裡我們透過下面的圖示來展示G-P-M 模型:

Go 為什麼這麼快”

##P 代表可以「並行「運行的邏輯處理器,每個P 都被分配到一個系統執行緒M,G 代表Go 協程。

Go 調度器中有兩個不同的運行佇列:全域運行佇列(GRQ)和本機運行佇列(LRQ)。

每個 P 都有一個 LRQ,用於管理分配給在 P 的上下文中執行的 Goroutines,這些 Goroutine 輪流被和 P 綁定的 M 進行上下文切換。 GRQ 適用於尚未指派給 P 的 Goroutines。

從上圖可以看出,G 的數量可以遠遠大於 M 的數量,換句話說,Go 程式可以利用少量的內核級線程來支撐大量 Goroutine 的並發。多個 Goroutine 透過使用者層級的上下文切換來共享核心執行緒 M 的運算資源,但對於作業系統來說並沒有執行緒上下文切換產生的效能損耗。

為了更充分利用執行緒的運算資源,Go 調度器採取了以下幾種調度策略:

任務竊取(work-stealing)

我們知道,現實情況有的Goroutine 運行的快,有的慢,那麼勢必肯定會帶來的問題就是,忙的忙死,閒的閒死,Go 肯定不允許摸魚的P 存在,勢必要充分利用好計算資源。

為了提高 Go 並行處理能力,調高整體處理效率,當每個 P 之間的 G 任務不均衡時,調度器允許從 GRQ,或其他 P 的 LRQ 中取得 G 執行。

減少阻塞

如果正在執行的 Goroutine 阻塞了執行緒 M 怎麼辦? P 上 LRQ 中的 Goroutine 會取得不到調度麼?

在Go 裡面阻塞主要分為一下4 個場景:

場景1:由於原子、互斥量或通道操作呼叫導致 Goroutine  阻塞,調度器將把目前阻塞的Goroutine 切換出去,重新調度LRQ 上的其他Goroutine;

場景2:由於網路請求和IO 操作導致 Goroutine  阻塞,這種阻塞的情況下,我們的G 和M 又會怎麼做呢?

Go 程式提供了網路輪詢器(NetPoller)來處理網路請求和IO 操作的問題,其後台透過kqueue(MacOS),epoll(Linux)或 iocp(Windows)來實現IO 多路復用。

透過使用 NetPoller 進行網路系統調用,調度器可防止  Goroutine  在進行這些系統調用時阻塞 M。這可以讓 M 執行 P 的  LRQ  中其他的  Goroutines,而不需要創造新的 M。有助於減少作業系統上的調度負載。

下圖展示它的運作方式:G1 正在 M 上執行,還有 3 個 Goroutine 在 LRQ 上等待執行。網路輪詢器空閒著,什麼都沒乾。

Go 為什麼這麼快”

接下來,G1 想要進行網路系統調用,因此它被移動到網路輪詢器並且處理非同步網路系統調用。然後,M 可以從 LRQ 執行另外的 Goroutine。此時,G2 就被上下文切換到 M 上了。

Go 為什麼這麼快”

最後,非同步網路系統呼叫由網路輪詢器完成,G1 被移回 P 的 LRQ 中。一旦 G1 可以在 M 上進行上下文切換,它負責的 Go 相關程式碼就可以再次執行。這裡的最大優勢是,執行網路系統呼叫不需要額外的 M。網路輪詢器使用系統線程,它時刻處理一個有效的事件循環。

Go 為什麼這麼快”

這種呼叫方式看起來很複雜,值得慶幸的是,Go 語言將該「複雜性」隱藏在Runtime 中:Go 開發者無需關注socket 是否是non-block 的,也無需親自註冊文件描述符的回調,只需在每個連接對應的Goroutine 中以“block I/O”的方式對待socket 處理即可,實現了goroutine-per-connection 簡單的網絡程式模式(但大量的Goroutine 也會帶來額外的問題,例如堆疊記憶體增加和調度器負擔加重)。

用戶層眼中看到的 Goroutine 中的“block socket”,實際上是透過 Go runtime 中的 netpoller 透過 Non-block socket I/O 多路復用機制“模擬”出來的。 Go 中的 net 函式庫正是按照這方式實現的。

場景3:當一些系統方法被呼叫的時候,如果系統方法呼叫的時候發生阻塞,這種情況下,網路輪詢器(NetPoller)無法使用,而進行系統呼叫的 Goroutine  將阻塞目前M。

讓我們來看看同步系統呼叫(如檔案 I/O)會導致 M 阻塞的情況:G1 將進行同步系統呼叫以阻塞 M1。

Go 為什麼這麼快”

調度器介入後:辨識出 G1 已導致 M1 阻塞,此時,調度器將 M1 與 P 分離,同時也將 G1 帶走。然後調度器引入新的 M2 來服務 P。此時,可以從 LRQ 中選擇 G2 並在 M2 上進行上下文切換。

Go 為什麼這麼快”

阻塞的系統呼叫完成後:G1 可以移回 LRQ 並再次由 P 執行。如果這種情況再次發生,M1 將被放在旁邊以備將來重複使用。

Go 為什麼這麼快”

場景 4:如果在 Goroutine 去執行一個 sleep 操作,導致 M 被阻塞了。

Go 程式後台有一個監控執行緒 sysmon,它監控那些長時間運行的 G 任務然後設定可以強佔的標識符,別的 Goroutine 就可以搶先進來執行。

只要下次這個 Goroutine 進行函數調用,那麼就會被強佔,同時也會保護現場,然後重新放入 P 的本地隊列裡面等待下次執行。

小結

本文主要從 Go 調度器架構層面上介紹了 G-P-M 模型,透過該模型如何實現少量核心執行緒支撐大量 Goroutine 的並發運行。以及透過 NetPoller、sysmon 等幫助 Go 程式減少執行緒阻塞,充分利用現有的運算資源,從而最大限度地提高 Go 程式的運作效率。

更多go語言知識請關注php中文網go語言教學欄位。

以上是Go 為什麼這麼'快”的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌
威爾R.E.P.O.有交叉遊戲嗎?
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

深入理解 Golang 函數生命週期與變數作用域 深入理解 Golang 函數生命週期與變數作用域 Apr 19, 2024 am 11:42 AM

在Go中,函數生命週期包括定義、載入、連結、初始化、呼叫和返回;變數作用域分為函數級和區塊級,函數內的變數在內部可見,而區塊內的變數僅在區塊內可見。

Go WebSocket 訊息如何發送? Go WebSocket 訊息如何發送? Jun 03, 2024 pm 04:53 PM

在Go中,可以使用gorilla/websocket包發送WebSocket訊息。具體步驟:建立WebSocket連線。傳送文字訊息:呼叫WriteMessage(websocket.TextMessage,[]byte("訊息"))。發送二進位訊息:呼叫WriteMessage(websocket.BinaryMessage,[]byte{1,2,3})。

如何在 Go 中使用正規表示式匹配時間戳記? 如何在 Go 中使用正規表示式匹配時間戳記? Jun 02, 2024 am 09:00 AM

在Go中,可以使用正規表示式比對時間戳記:編譯正規表示式字串,例如用於匹配ISO8601時間戳記的表達式:^\d{4}-\d{2}-\d{2}T \d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-][0-9]{2}:[0-9]{2})$ 。使用regexp.MatchString函數檢查字串是否與正規表示式相符。

Golang 與 Go 語言的區別 Golang 與 Go 語言的區別 May 31, 2024 pm 08:10 PM

Go和Go語言是不同的實體,具有不同的特性。 Go(又稱Golang)以其並發性、編譯速度快、記憶體管理和跨平台優點而聞名。 Go語言的缺點包括生態系統不如其他語言豐富、文法更嚴格、缺乏動態類型。

Golang 技術效能優化中如何避免記憶體洩漏? Golang 技術效能優化中如何避免記憶體洩漏? Jun 04, 2024 pm 12:27 PM

記憶體洩漏會導致Go程式記憶體不斷增加,可通過:關閉不再使用的資源,如檔案、網路連線和資料庫連線。使用弱引用防止記憶體洩漏,當物件不再被強引用時將其作為垃圾回收目標。利用go協程,協程棧記憶體會在退出時自動釋放,避免記憶體洩漏。

如何在 IDE 中查看 Golang 函數文件? 如何在 IDE 中查看 Golang 函數文件? Apr 18, 2024 pm 03:06 PM

使用IDE檢視Go函數文件:將遊標停留在函數名稱上。按下熱鍵(GoLand:Ctrl+Q;VSCode:安裝GoExtensionPack後,F1並選擇"Go:ShowDocumentation")。

Go 並發函數的單元測試指南 Go 並發函數的單元測試指南 May 03, 2024 am 10:54 AM

對並發函數進行單元測試至關重要,因為這有助於確保其在並發環境中的正確行為。測試並發函數時必須考慮互斥、同步和隔離等基本原理。可以透過模擬、測試競爭條件和驗證結果等方法對並發函數進行單元測試。

如何使用 Golang 的錯誤包裝器? 如何使用 Golang 的錯誤包裝器? Jun 03, 2024 pm 04:08 PM

在Golang中,錯誤包裝器允許你在原始錯誤上追加上下文訊息,從而創建新錯誤。這可用於統一不同程式庫或元件拋出的錯誤類型,簡化偵錯和錯誤處理。步驟如下:使用errors.Wrap函數將原有錯誤包裝成新錯誤。新錯誤包含原始錯誤的上下文資訊。使用fmt.Printf輸出包裝後的錯誤,提供更多上下文和可操作性。在處理不同類型的錯誤時,使用errors.Wrap函數統一錯誤類型。

See all articles