Go的CSP并發(fā)模型
創(chuàng)新互聯(lián)從2013年成立,先為臨滄等服務(wù)建站,臨滄等地企業(yè),進(jìn)行企業(yè)商務(wù)咨詢服務(wù)。為臨滄企業(yè)網(wǎng)站制作PC+手機(jī)+微官網(wǎng)三網(wǎng)同步一站式服務(wù)解決您的所有建站問題。
Go實(shí)現(xiàn)了兩種并發(fā)形式。第一種是大家普遍認(rèn)知的:多線程共享內(nèi)存。其實(shí)就是Java或者C++等語言中的多線程開發(fā)。另外一種是Go語言特有的,也是Go語言推薦的:CSP(communicating sequential processes)并發(fā)模型。
CSP 是 Communicating Sequential Process 的簡(jiǎn)稱,中文可以叫做通信順序進(jìn)程,是一種并發(fā)編程模型,由 Tony Hoare 于 1977 年提出。簡(jiǎn)單來說,CSP 模型由并發(fā)執(zhí)行的實(shí)體(線程或者進(jìn)程)所組成,實(shí)體之間通過發(fā)送消息進(jìn)行通信,這里發(fā)送消息時(shí)使用的就是通道,或者叫 channel。CSP 模型的關(guān)鍵是關(guān)注 channel,而不關(guān)注發(fā)送消息的實(shí)體。 Go 語言實(shí)現(xiàn)了 CSP 部分理論 。
“ 不要以共享內(nèi)存的方式來通信,相反, 要通過通信來共享內(nèi)存?!?/p>
Go的CSP并發(fā)模型,是通過 goroutine和channel 來實(shí)現(xiàn)的。
goroutine 是Go語言中并發(fā)的執(zhí)行單位。其實(shí)就是協(xié)程。
channel是Go語言中各個(gè)并發(fā)結(jié)構(gòu)體(goroutine)之前的通信機(jī)制。 通俗的講,就是各個(gè)goroutine之間通信的”管道“,有點(diǎn)類似于Linux中的管道。
Channel
Goroutine
參考:
Goroutine并發(fā)調(diào)度模型深度解析手?jǐn)]一個(gè)協(xié)程池
Golang 的 goroutine 是如何實(shí)現(xiàn)的?
Golang - 調(diào)度剖析【第二部分】
OS線程初始棧為2MB。Go語言中,每個(gè)goroutine采用動(dòng)態(tài)擴(kuò)容方式,初始2KB,按需增長,最大1G。此外GC會(huì)收縮??臻g。
BTW,增長擴(kuò)容都是有代價(jià)的,需要copy數(shù)據(jù)到新的stack,所以初始2KB可能有些性能問題。
更多關(guān)于stack的內(nèi)容,可以參見大佬的文章。 聊一聊goroutine stack
用戶線程的調(diào)度以及生命周期管理都是用戶層面,Go語言自己實(shí)現(xiàn)的,不借助OS系統(tǒng)調(diào)用,減少系統(tǒng)資源消耗。
Go語言采用兩級(jí)線程模型,即用戶線程與內(nèi)核線程KSE(kernel scheduling entity)是M:N的。最終goroutine還是會(huì)交給OS線程執(zhí)行,但是需要一個(gè)中介,提供上下文。這就是G-M-P模型
Go調(diào)度器有兩個(gè)不同的運(yùn)行隊(duì)列:
go1.10\src\runtime\runtime2.go
Go調(diào)度器根據(jù)事件進(jìn)行上下文切換。
調(diào)度的目的就是防止M堵塞,空閑,系統(tǒng)進(jìn)程切換。
詳見 Golang - 調(diào)度剖析【第二部分】
Linux可以通過epoll實(shí)現(xiàn)網(wǎng)絡(luò)調(diào)用,統(tǒng)稱網(wǎng)絡(luò)輪詢器N(Net Poller)。
文件IO操作
上面都是防止M堵塞,任務(wù)竊取是防止M空閑
每個(gè)M都有一個(gè)特殊的G,g0。用于執(zhí)行調(diào)度,gc,棧管理等任務(wù),所以g0的棧稱為調(diào)度棧。g0的棧不會(huì)自動(dòng)增長,不會(huì)被gc,來自os線程的棧。
go1.10\src\runtime\proc.go
G沒辦法自己運(yùn)行,必須通過M運(yùn)行
M通過通過調(diào)度,執(zhí)行G
從M掛載P的runq中找到G,執(zhí)行G
Go有四大核心模塊,基本全部體現(xiàn)在runtime,有調(diào)度系統(tǒng)、GC、goroutine、channel,那么深入理解其中的精髓可以幫助我們理解Go這一門語言!
參考: 調(diào)度系統(tǒng)設(shè)計(jì)精要
下面是我用Go語言簡(jiǎn)單寫的一個(gè)調(diào)度器,大家可以看看設(shè)計(jì)思路,以及存在的問題!
1、測(cè)試條件,調(diào)度器只啟動(dòng)兩個(gè)線程,然后一個(gè)線程主要是負(fù)責(zé)循環(huán)的添加任務(wù),一個(gè)線程循環(huán)的去執(zhí)行任務(wù)
2、測(cè)試條件,調(diào)度器啟動(dòng)三個(gè)線程,然后兩個(gè)線程去執(zhí)行任務(wù),一個(gè)添加任務(wù)
3、繼續(xù)測(cè)試,啟動(dòng)十個(gè)線程,一個(gè)添加任務(wù),九個(gè)執(zhí)行任務(wù)
4、我們添加一些阻塞的任務(wù)
執(zhí)行可以看到完全不可用
1、 可以看到隨著M的不斷的增加,可以發(fā)現(xiàn)執(zhí)行任務(wù)的數(shù)量也不斷的減少,原因是什么呢?有興趣的同學(xué)可以加一個(gè)pprof可以看看,其實(shí)大量的在等待鎖的過程!
2、如果我的M運(yùn)行了類似于Sleep操作的方法如何解決了,我的調(diào)度器還能支撐這個(gè)量級(jí)的調(diào)度嗎?
關(guān)于pprof如何使用:在代碼頭部加一個(gè)這個(gè)代碼:
我們查看一下 go tool pprof main/prof.pporf
可以看到真正執(zhí)行代碼的時(shí)間只有 0.17s + 0.02s 其他時(shí)間都被阻塞掉了!
1、GM模型中的所有G都是放入到一個(gè)queue,那么導(dǎo)致所有的M取執(zhí)行任務(wù)時(shí)都會(huì)去競(jìng)爭(zhēng)鎖,我們插入G也會(huì)去競(jìng)爭(zhēng)鎖,所以解決這種問題一般就是減少對(duì)單一資源的競(jìng)爭(zhēng),那就是桶化,其實(shí)就是每個(gè)線程都分配一個(gè)隊(duì)列
2、GM模型中沒有任務(wù)狀態(tài),只有runnable,假如任務(wù)遇到阻塞,完全可以把任務(wù)掛起再喚醒
這里其實(shí)會(huì)遇到一個(gè)問題,假如要分配很多個(gè)線程,那么此時(shí)隨著線程的增加,也會(huì)造成隊(duì)列的增加,其實(shí)也會(huì)造成調(diào)度器的壓力,因?yàn)樗枰闅v全部線程的隊(duì)列去分配任務(wù)以及后續(xù)會(huì)講到的竊取任務(wù)!
因?yàn)槲覀冎繡PU的最大并行度其實(shí)取決于CPU的核數(shù),也就是我們沒必要為每個(gè)線程都去分配一個(gè)隊(duì)列,因?yàn)榫退闶墙o他們分配了,他們自己去那執(zhí)行調(diào)度,其實(shí)也會(huì)出現(xiàn)大量阻塞,原因就是CPU調(diào)度不過來這些線程!
Go里面是只分配了CPU個(gè)數(shù)的隊(duì)列,這里就是P這個(gè)概念,你可以理解為P其實(shí)是真正的資源分配器,M很輕只是執(zhí)行程序,所有的資源內(nèi)存都維護(hù)在P上!M只有綁定P才能執(zhí)行任務(wù)(強(qiáng)制的)!
這樣做的好處:
1、首先調(diào)度程序其實(shí)就是調(diào)度不同狀態(tài)的任務(wù),go里面為Go標(biāo)記了不同的狀態(tài),其實(shí)大概就是分為:runnable,running,block等,所以如何充分調(diào)度不同狀態(tài)的G成了問題,那么關(guān)于阻塞的G如何解決,其實(shí)可以很好的解決G調(diào)度的問題!
上面這些情況其實(shí)就分為:
2、用戶態(tài)阻塞,一般Go里面依靠 gopark 函數(shù)去實(shí)現(xiàn),大體的代碼邏輯基本上和go的調(diào)度綁定死了
源碼在:
3、其實(shí)對(duì)于netpool 這種nio模型,其實(shí)內(nèi)核調(diào)用是非阻塞的,所以go開辟了一個(gè)網(wǎng)絡(luò)輪訓(xùn)器隊(duì)列,來存放這些被阻塞的g,等待內(nèi)核被喚醒!那么什么時(shí)候會(huì)被喚醒了,其實(shí)就是需要等待調(diào)度器去調(diào)度了!
4、如果是內(nèi)核態(tài)阻塞了(內(nèi)核態(tài)阻塞一般都會(huì)將線程掛起,線程需要等待被喚醒),我們此時(shí)P只能放棄此線程的權(quán)利,然后再找一個(gè)新的線程去運(yùn)行P!
關(guān)于著新線程:找有沒有idle的線程,沒有就會(huì)創(chuàng)建一個(gè)新的線程!
關(guān)于當(dāng)內(nèi)核被喚醒后的操作:因?yàn)镚PM模型所以需要找到個(gè)P綁定,所以G會(huì)去嘗試找一個(gè)可用的P,如果沒有可用的P,G會(huì)標(biāo)記為runnable放到全局隊(duì)列中!
5、其實(shí)了解上面大致其實(shí)就了解了Go的基本調(diào)度模型
答案文章里慢慢品味!
如果某個(gè) G 執(zhí)行時(shí)間過長,其他的 G 如何才能被正常的調(diào)度? 這便涉及到有關(guān)調(diào)度的兩個(gè)理念:協(xié)作式調(diào)度與搶占式調(diào)度。協(xié)作式和搶占式這兩個(gè)理念解釋起來很簡(jiǎn)單: 協(xié)作式調(diào)度依靠被調(diào)度方主動(dòng)棄權(quán);搶占式調(diào)度則依靠調(diào)度器強(qiáng)制將被調(diào)度方被動(dòng)中斷。
例如下面的代碼,我本地的版本是 go1.13.5
執(zhí)行: GOMAXPROCS=1 配置全局只能有一個(gè)P
可以看到main函數(shù)無法執(zhí)行!也就是那個(gè)go 空轉(zhuǎn)搶占了整個(gè)程序
備注:
但是假如我換為用 1.14+版本執(zhí)行,有興趣的話可以使用我的docker鏡像,直接可以拉取: fanhaodong/golang:1.15.11 和 fanhaodong/golang:1.13.5
首先我們知道G/M/P,G可能和M也可能和P解除綁定,那么關(guān)于數(shù)據(jù)變量放在哪哇!其實(shí)這個(gè)就是逃逸分析!
輸出可以看到其實(shí)沒有發(fā)生逃逸,那是因?yàn)?demo被拷貝它自己的棧空間內(nèi)
備注:
-gcflags"-N -l -m" 其中 -N禁用優(yōu)化-l禁止內(nèi)聯(lián)優(yōu)化,-m打印逃逸信息
那么繼續(xù)改成這個(gè)
可以看到發(fā)現(xiàn) demo對(duì)象其實(shí)被逃逸到了堆上!這就是不會(huì)出現(xiàn)類似于G如果被別的M執(zhí)行,其實(shí)不會(huì)出現(xiàn)內(nèi)存分配位置的問題!
所以可以看到demo其實(shí)是copy到了堆上!這就是g逃逸的問題,和for循環(huán)一樣的
執(zhí)行可以發(fā)現(xiàn),其實(shí)x已經(jīng)逃逸到了堆上,所以你所有的g都引用的一個(gè)對(duì)象,如何解決了
如何解決了,其實(shí)很簡(jiǎn)單
也談goroutine調(diào)度器
圖解Go運(yùn)行時(shí)調(diào)度器
Go語言回顧:從Go 1.0到Go 1.13
Go語言原本
調(diào)度系統(tǒng)設(shè)計(jì)精要
Scalable Go Scheduler Design Doc
內(nèi)核線程(Kernel-Level Thread ,KLT)
輕量級(jí)進(jìn)程(Light Weight Process,LWP):輕量級(jí)進(jìn)程就是我們通常意義上所講的線程,由于每個(gè)輕量級(jí)進(jìn)程都由一個(gè)內(nèi)核線程支持,因此只有先支持內(nèi)核線程,才能有輕量級(jí)進(jìn)程
用戶線程與系統(tǒng)線程一一對(duì)應(yīng),用戶線程執(zhí)行如lo操作的系統(tǒng)調(diào)用時(shí),來回切換操作開銷相對(duì)比較大
多個(gè)用戶線程對(duì)應(yīng)一個(gè)內(nèi)核線程,當(dāng)內(nèi)核線程對(duì)應(yīng)的一個(gè)用戶線程被阻塞掛起時(shí)候,其他用戶線程也阻塞不能執(zhí)行了。
多對(duì)多模型是可以充分利用多核CPU提升運(yùn)行效能的
go線程模型包含三個(gè)概念:內(nèi)核線程(M),goroutine(G),G的上下文環(huán)境(P);
GMP模型是goalng特有的。
P與M一般是一一對(duì)應(yīng)的。P(上下文)管理著一組G(goroutine)掛載在M(內(nèi)核線程)上運(yùn)行,圖中左邊藍(lán)色為正在執(zhí)行狀態(tài)的goroutine,右邊為待執(zhí)行狀態(tài)的goroutiine隊(duì)列。P的數(shù)量由環(huán)境變量GOMAXPROCS的值或程序運(yùn)行runtime.GOMAXPROCS()進(jìn)行設(shè)置。
當(dāng)一個(gè)os線程在執(zhí)行M1一個(gè)G1發(fā)生阻塞時(shí),調(diào)度器讓M1拋棄P,等待G1返回,然后另起一個(gè)M2接收P來執(zhí)行剩下的goroutine隊(duì)列(G2、G3...),這是golang調(diào)度器厲害的地方,可以保證有足夠的線程來運(yùn)行剩下所有的goroutine。
當(dāng)G1結(jié)束后,M1會(huì)重新拿回P來完成,如果拿不到就丟到全局runqueue中,然后自己放到線程池或轉(zhuǎn)入休眠狀態(tài)??臻e的上下文P會(huì)周期性的檢查全局runqueue上的goroutine,并且執(zhí)行它。
另一種情況就是當(dāng)有些P1太閑而其他P2很忙碌的時(shí)候,會(huì)從其他上下文P2拿一些G來執(zhí)行。
詳細(xì)可以翻看下方第一個(gè)參考鏈接,寫得真好。
最后用大佬的總結(jié)來做最后的收尾————
Go語言運(yùn)行時(shí),通過核心元素G,M,P 和 自己的調(diào)度器,實(shí)現(xiàn)了自己的并發(fā)線程模型。調(diào)度器通過對(duì)G,M,P的調(diào)度實(shí)現(xiàn)了兩級(jí)線程模型中操作系統(tǒng)內(nèi)核之外的調(diào)度任務(wù)。整個(gè)調(diào)度過程中會(huì)在多種時(shí)機(jī)去觸發(fā)最核心的步驟 “一整輪調(diào)度”,而一整輪調(diào)度中最關(guān)鍵的部分在“全力查找可運(yùn)行G”,它保證了M的高效運(yùn)行(換句話說就是充分使用了計(jì)算機(jī)的物理資源),一整輪調(diào)度中還會(huì)涉及到M的啟用停止。最后別忘了,還有一個(gè)與Go程序生命周期相同的系統(tǒng)監(jiān)測(cè)任務(wù)來進(jìn)行一些輔助性的工作。
淺析Golang的線程模型與調(diào)度器
Golang CSP并發(fā)模型
Golang線程模型