久久国产乱子伦精品免费M,亚洲一区二区三区91,欧美国产在线视频,国产精品视频久久

Goroutine 是 Go 的并發(fā)機(jī)制中 絕對的主角

由于對普通語法的介紹網(wǎng)上資源極多,Go 官方的上手指南 A Tour of Go: https://tour.golang.org/ (請自備梯子)就是極好的例子,我不再打算就語法細(xì)節(jié)進(jìn)行詳述。這次,讓我們直切肯綮,從 Go 最大的賣點(diǎn)入手——并發(fā) (Concurrency)。

如果在雙核(及以上)的機(jī)器編譯運(yùn)行上述 Go 代碼,我們能觀測到 A/B 輸出的順序隨著運(yùn)行次數(shù)的不同而不同,也就是說,僅依靠 5 行代碼,我們就創(chuàng)建了兩線并發(fā)的程序。

相較于 C/C++/Java/Python 等語言為了創(chuàng)建一個(gè)并發(fā)執(zhí)行環(huán)境所需要的調(diào)用 POSIX-API/定義繼承類等繁瑣步驟,Golang 簡單一句 go func()的確給人眼前一亮的感覺。當(dāng)然了,僅憑語法上的簡潔顯然不足以成為一個(gè)編程語言拿來吹噓的資本,下文我們將對在這幾行語句下 Golang 的并發(fā)機(jī)制和實(shí)現(xiàn)進(jìn)行詳細(xì)探索。

一等公民-Goroutine

Goroutine 是 Go 的并發(fā)機(jī)制中絕對的主角。它代表了指令流及其執(zhí)行環(huán)境,也是被調(diào)度的基本單位。宏觀來看,goroutine 類似操作系統(tǒng)中線程的概念(注意這里的類比并不嚴(yán)格,下文將會(huì)對兩者做出詳細(xì)比較):不同線程間共享同一個(gè)內(nèi)存空間,但不共享?xiàng)G腋髯圆l(fā)執(zhí)行;同樣地,goroutine 也同內(nèi)存不同棧,并發(fā)運(yùn)行。

Goroutine 是 Go 的并發(fā)機(jī)制中 絕對的主角

如上圖所示,上文代碼片段第四行的 go Hello()會(huì)創(chuàng)建一個(gè)新的 goroutine(綠色線條),并開始執(zhí)行 Hello()函數(shù)。需要注意的是,由于主 goroutine(藍(lán)色線條)和新創(chuàng)建的 goroutine 擁有并發(fā)性,且主 goroutine 在執(zhí)行 go Hello()時(shí)并不會(huì)等待被調(diào)用函數(shù)執(zhí)行結(jié)束,故“I'm A”(主 goroutine 輸出)和“I'm B”(新 goroutine 輸出)可能以任何順序交錯(cuò)展現(xiàn)。

?

為何不用線程 (pThread)?

直到現(xiàn)在,我們并不能從 goroutine 中看到任何有別于 thread、從而促成 Golang 編寫者拋棄傳統(tǒng)的線程模型自己造輪子的地方。那么操作系統(tǒng)層面的線程 (pThread) 有什么問題呢?

生命周期開銷太高

線程的創(chuàng)建、銷毀和切換都需要一系列系統(tǒng)調(diào)用,而每一個(gè)系統(tǒng)調(diào)用意味著觸發(fā)軟中斷、進(jìn)入內(nèi)核態(tài)、將寄存器的值全部存入內(nèi)存、維護(hù)相關(guān)數(shù)據(jù)結(jié)構(gòu)、恢復(fù)寄存器、返回用戶態(tài)等一系列組合拳。這一輪操作不僅十分耗時(shí)、還可能讓內(nèi)存緩存的加速效果大幅度下滑。所以,避免頻繁創(chuàng)建、銷毀線程作為高性能并發(fā)的必要條件這一點(diǎn)已成為程序員的共識(shí)。

以線程為并發(fā)模型的 C/C++/Java 采用線程池的方法來降低線程昂貴的生命周期開銷。既然線程創(chuàng)建/死亡代價(jià)高昂,我們何不讓創(chuàng)建的線程永不死亡呢?具體來說,對于每個(gè)已經(jīng)創(chuàng)建但已經(jīng)完成工作的線程,我們令其休眠,并放進(jìn)一個(gè)資源池中,在下次需要新的線程的時(shí)候,我們直接將線程池中休眠的線程拿出來喚醒使用而非新建線程。

這樣一來,絕大部分的線程創(chuàng)建/銷毀需求都成功地被線程池吸收了。進(jìn)一步,通過規(guī)定線程池的最大容量,我們可以將花費(fèi)在線程創(chuàng)建和銷毀上的開銷控制在固定值,例如,常見的 Java Web 應(yīng)用會(huì)設(shè)立一個(gè) 30~50 大小的線程池來處理 HTTP 請求,并取得非常好的并發(fā)效果。

不必要的線程切換

即使線程池很好地砍掉了線程生命周期開銷,操作系統(tǒng)層面的線程依然存在不足:線程的語義在于并行,當(dāng)線程數(shù)超出 CPU 核心數(shù)時(shí),操作系統(tǒng)會(huì)定時(shí)給每個(gè) CPU 核心切換不同的線程,讓他們“看上去”是同時(shí)在進(jìn)行的。當(dāng)然,這樣的切換同樣需要付出若干中斷、系統(tǒng)調(diào)用,以及當(dāng)前線程的工作集從緩存中被新線程完全抹去的代價(jià)。

乍一聽上去這樣的代價(jià)是必不可少的,實(shí)則不然。由于在絕大部分時(shí)候我們的應(yīng)用都是 I/O 和計(jì)算混合的,即,一段時(shí)間與硬盤/網(wǎng)絡(luò)交互(I/O)、一段時(shí)間進(jìn)行相對密集的內(nèi)存訪問和計(jì)算,而等待 I/O 完成期間該線程處于休眠狀態(tài),CPU 已經(jīng)會(huì)切換到其他線程,即使操作系統(tǒng)不強(qiáng)行打斷并切換處于計(jì)算密集期的線程,應(yīng)用在宏觀上依然顯示出一定并發(fā)性。

而通過去掉計(jì)算密集期的線程切換,整體 CPU 效率得到了有效提升——NodeJS 就是在這樣的哲學(xué)下誕生的:單一線程、全異步的 I/O、事件驅(qū)動(dòng)、非搶占式調(diào)度(當(dāng)某一個(gè)函數(shù)單純進(jìn)行計(jì)算和內(nèi)存訪問時(shí)不會(huì)被打斷),在進(jìn)行 I/O 密集型工作(如網(wǎng)站后臺(tái))時(shí)通過將單一 CPU 利用率逼到 100%的方式在效率上力挫幾乎其他所有能利用多線程多核腳本語言。

這簡直是本來就特立獨(dú)行的 Javascript 對整個(gè)編程語言界的同僚豎起的又一根中指。當(dāng)然了,僅僅能利用單核處理能力的 NodeJS 在處理對計(jì)算要求更高的工作上顯然會(huì)力不從心,但其給我們的啟示值得注意。

較高的切換開銷

在鎖競爭、協(xié)程同步等情況下,頻繁進(jìn)入內(nèi)核態(tài)的線程模型會(huì)放大自身在切換開銷上的劣勢。而用戶態(tài)的調(diào)度器(如 goroutine 調(diào)度器)則可以在用戶態(tài)處理這一切,省時(shí)省力。另外,由于編程語言能夠更好地對自己語言中的同步原語進(jìn)行分析,編程語言自己的調(diào)度器能夠更好地根據(jù)語義對調(diào)度進(jìn)行優(yōu)化。

Goroutine 調(diào)度模型

Go 使用用戶態(tài)的調(diào)度器對 goroutine 的執(zhí)行進(jìn)行控制,從而避免了大部分內(nèi)核開銷。具體而言,Golang 的調(diào)度模型由三部分組成:執(zhí)行環(huán)境 (Executor)、調(diào)度器 (Scheduler)?和 goroutine。

執(zhí)行環(huán)境,顧名思義,用來執(zhí)行代碼。盡管其在抽象概念上應(yīng)該對應(yīng)一個(gè) CPU 核心,但由于在用戶態(tài)不能接觸硬件資源,故 Go 將其具體實(shí)現(xiàn)為線程。當(dāng)線程數(shù)等于 CPU 核心數(shù)時(shí),既最大化了 CPU 核心利用率,又最小化了線程切換的開銷,是最理想的情況(當(dāng)然,實(shí)際情況下操作系統(tǒng)還會(huì)運(yùn)行、切換來自其他進(jìn)程的線程,但這已經(jīng)超出一個(gè)普通程序的控制范疇)。

故默認(rèn)情況下,用于指定執(zhí)行環(huán)境個(gè)數(shù)的運(yùn)行時(shí)變量 GOMAXPROCS等于 CPU 核心數(shù)目。當(dāng)然,開發(fā)者可以根據(jù)自己的需求更改該值,當(dāng) GOMAXPROCS=1時(shí),Go 的執(zhí)行模型幾乎等同于 NodeJS。

調(diào)度器則是調(diào)度模型的核心,它決定了每個(gè)執(zhí)行環(huán)境(核)在什么時(shí)候執(zhí)行什么樣的 goroutine。Go 采用任務(wù)隊(duì)列的方式對 goroutine 進(jìn)行調(diào)度:

Goroutine 是 Go 的并發(fā)機(jī)制中 絕對的主角

如上圖所示,所有 goroutine 作為任務(wù)排在任務(wù)隊(duì)列中,而 scheduler 所做的則是在 executor 空閑時(shí)從隊(duì)首拿出下一個(gè) goroutine 給其執(zhí)行。每個(gè)任務(wù) (goroutine) 會(huì)被 executor 執(zhí)行到完成或阻塞(如發(fā)起 I/O 請求、系統(tǒng)調(diào)用、請求一個(gè)正在被其他人使用的鎖或自行 yield 計(jì)算資源等),在第二種情況下,該 goroutine 既不在 executor 也不在隊(duì)列中,而是處于阻塞態(tài)被 Scheduler 監(jiān)視直到阻塞結(jié)束重新入隊(duì)。

值得注意的是,這里與上文提到的“去掉計(jì)算密集期的線程切換”的聯(lián)系:由于調(diào)度器對任務(wù)采用非搶占式調(diào)度,即在正常計(jì)算和內(nèi)存訪問的情況下 executor 不會(huì)放棄當(dāng)前 goroutine,故多余的 goroutine 切換代價(jià)得以被去除。

這樣的任務(wù)隊(duì)列模型仍然存在不小的問題:由于任務(wù)隊(duì)列只有一個(gè),為了保證出入隊(duì)的原子性,任務(wù)分配/加入時(shí)需要對整個(gè)隊(duì)列加互斥鎖,當(dāng) goroutine 執(zhí)行時(shí)間短時(shí),頻繁給大量 executor 分配新任務(wù)會(huì)讓單一隊(duì)列成為并行的性能瓶頸。為了解決該問題,Go 采用了多任務(wù)隊(duì)列的方式進(jìn)行任務(wù)調(diào)度:

Goroutine 是 Go 的并發(fā)機(jī)制中 絕對的主角

如上圖所示,在多任務(wù)調(diào)度模型中,每個(gè) executor 均有一個(gè)自己對應(yīng)的任務(wù)隊(duì)列。在正常情況下,每個(gè) executor 從自己的隊(duì)列中拿 goroutine,并將生成的新 goroutine 放進(jìn)自己隊(duì)列隊(duì)尾。分布式結(jié)構(gòu)可能帶來的問題是顯而易見的:如果任務(wù)在隊(duì)列的分布不均勻會(huì)導(dǎo)致計(jì)算資源的浪費(fèi),如上圖中的 executor3,如果缺乏其他措施,該核會(huì)因?yàn)閷?yīng)隊(duì)列沒有任務(wù)而空閑。

對于該問題,Go 的解決方法是引入“偷任務(wù)”機(jī)制:當(dāng) Scheduler 發(fā)現(xiàn)某隊(duì)列無任務(wù)可用時(shí),會(huì)從其他隊(duì)列里“偷”一部分任務(wù)過來。由于偷任務(wù)的代價(jià)較高(需要鎖兩個(gè)隊(duì)列),Scheduler 會(huì)爭取一次性偷足夠多的任務(wù)以降低未來偷任務(wù)的頻率。

而對于處于阻塞狀態(tài)的 goroutine,Scheduler 需要監(jiān)視其脫離阻塞狀態(tài)并重新入隊(duì)。Goroutine 被阻塞的原因大體分兩種:

  • 阻塞 I/O 或系統(tǒng)調(diào)用。由于底層實(shí)現(xiàn)限制,該類阻塞需要一個(gè)線程顯式執(zhí)行相應(yīng)的 syscall 并等待調(diào)用返回。在這種情況下,Scheduler 會(huì)新建一個(gè)線程執(zhí)行該 syscall,并在返回后通知 Scheduler。同樣地,為了節(jié)省開銷,該線程被維護(hù)在線程池中。值得注意的是,該類線程由于整個(gè)生命周期都幾乎在等待阻塞(阻塞結(jié)束后立即通知 Scheduler 而后結(jié)束),而阻塞的線程是不參與操作系統(tǒng)線程切換的,故其并不會(huì)帶來太大的線程切換開銷。當(dāng)然,如果借鑒 NodeJS、盡可能用異步版本 api 替換同步版,則可以省去線程池操作,進(jìn)一步優(yōu)化性能(Go 是否采用該優(yōu)化尚存疑)。
  • 內(nèi)部同步機(jī)制,Goroutine 因?yàn)檎{(diào)用了 Go 內(nèi)部同步機(jī)制(channel、互斥鎖、wait group、conditional variable 等)而阻塞。對于此類阻塞,由于同步機(jī)制的語義是 Go 定義從而對 Scheduler 透明的,Scheduler 可以分析出阻塞依賴,從而將監(jiān)視該阻塞狀態(tài)的任務(wù)交給其依賴的 goroutine。例如,goroutine A 請求了一個(gè)正被 goroutine B 獲取了的互斥鎖,從而陷入阻塞,那么 Scheduler 可以在 goroutine B 釋放該鎖時(shí)由對應(yīng)的 executor 將 goroutine A 喚醒并加入隊(duì)列。在這整個(gè)過程中不需要引入新的線程。

以上便是 Golang Scheduler 的大致工作邏輯,在各個(gè)組件的相互配合下,一個(gè)高性能、支持調(diào)度成千上萬 goroutine 的并發(fā)環(huán)境就此搭建起來。

?

總結(jié)和啟發(fā)

從 Golang 的并發(fā)機(jī)制中我們可以得到如下幾點(diǎn)啟發(fā):

  • 系統(tǒng)調(diào)用和內(nèi)核態(tài)是昂貴的,用戶態(tài)的調(diào)度器擁有更好的性能。
  • 由于頻繁進(jìn)行不必要的切換,線程并不是合適的并發(fā)執(zhí)行基本單位;相反,將線程作為執(zhí)行資源 (CPU) 的抽象、為一個(gè) CPU 核心建立一個(gè)線程作為執(zhí)行器則是一個(gè)很不錯(cuò)的主意。
  • 單一任務(wù)隊(duì)列在任務(wù)短而多時(shí)劣勢明顯,分布式隊(duì)列+任務(wù)偷取能夠較好的解決問題。

可以說,Golang 的并發(fā)機(jī)制是 NodeJS 的普適版,擁有能夠更好利用多核計(jì)算力的優(yōu)勢;和 采用 OS 線程、阻塞 I/O、GIL 的 Python 并發(fā)模式 相比則更是云泥之別。正是更為精巧的并發(fā)機(jī)制和簡單的并發(fā)原語,使得 Concurrency 成為 Go 語言最大的賣點(diǎn)。

需要指出的是,Go 所采用的一切技術(shù)都并非原創(chuàng)—— go func()的同步原語與 Cilk 十分類似,分布式任務(wù)隊(duì)列也多少有模仿 Cilk/OpenMP 的意味,如果非要說不同之處,大概在于 Go 是一個(gè)原生支持該功能的完整編程語言,而另外兩者只是 C/C++的語法擴(kuò)展插件吧。

原文鏈接:https://mp.weixin.qq.com/s/16W2VFdlH6En8-0EspZu4g

相關(guān)新聞

歷經(jīng)多年發(fā)展,已成為國內(nèi)好評(píng)如潮的Linux云計(jì)算運(yùn)維、SRE、Devops、網(wǎng)絡(luò)安全、云原生、Go、Python開發(fā)專業(yè)人才培訓(xùn)機(jī)構(gòu)!

    1. 主站蜘蛛池模板: 游戏| 陆良县| 哈巴河县| 安远县| 罗甸县| 克拉玛依市| 霸州市| 乌鲁木齐市| 南宫市| 天等县| 腾冲县| 新绛县| 银川市| 新巴尔虎右旗| 汉川市| 朝阳区| 咸宁市| 黄平县| 肃南| 达孜县| 白朗县| 德令哈市| 洛宁县| 含山县| 襄垣县| 印江| 蒙自县| 平凉市| 惠东县| 凤城市| 屏东市| 伊宁市| 萨迦县| 女性| 桐柏县| 迭部县| 科尔| 神木县| 安龙县| 沅江市| 麻栗坡县|