歡迎關(guān)注微信公眾號(hào):全棧工廠
Go語言的高并發(fā)性能一直是大家最關(guān)注的點(diǎn),Go語言把原本復(fù)雜的并發(fā)編程通過協(xié)程的方式更方便的提供給開發(fā)人員,讓其在云時(shí)代背景下有了得天獨(dú)厚的優(yōu)勢,我們可以通過GO語言的并發(fā)模型和調(diào)度模型來嘗試窺探其高并發(fā)性能背后的邏輯。
1 CSP并發(fā)模型
CSP是上個(gè)世紀(jì)七十年代提出的一種強(qiáng)大的并發(fā)編程模型,它的全稱是 Communicating Sequential Process 即:通信順序進(jìn)程,它的核心觀點(diǎn)是:
Do not communicate by sharing memory; instead, share memory by communicating
即:
不要以共享內(nèi)存的方式來通信,而是要通過通信來共享內(nèi)存

普通的線程并發(fā)模型(例如Java、C++等),他們線程間通信都是通過共享內(nèi)存的方式來進(jìn)行的;非常典型的方式就是,構(gòu)建一個(gè)全局共享數(shù)據(jù)(變量),通過加鎖機(jī)制來保證共享數(shù)據(jù)在并發(fā)環(huán)境下的線程安全,從而實(shí)現(xiàn)并發(fā)線程間的通信。

Go語言既提供了傳統(tǒng)的線程并發(fā)模型編程方式,也提供了CSP并發(fā)模型的實(shí)現(xiàn),在CSP并發(fā)模型中,每一個(gè)并發(fā)實(shí)體(Process)只需要關(guān)注消息發(fā)送的對(duì)象(Channel),而不用關(guān)注另一個(gè)并發(fā)實(shí)體,這使得并發(fā)實(shí)體間實(shí)現(xiàn)了完全解耦,這兩個(gè)并發(fā)原語之間沒有從屬關(guān)系, Process 可以訂閱任意個(gè) Channel,Channel 也并不關(guān)心是哪個(gè) Process 在利用它進(jìn)行通信;Process 圍繞 Channel 進(jìn)行讀寫,形成一套有序阻塞和可預(yù)測的并發(fā)模型。
2 MPG調(diào)度模型
操作系統(tǒng)對(duì)線程的并發(fā)調(diào)度直接決定了能否對(duì)硬件資源進(jìn)行合理充分的利用,操作系統(tǒng)根據(jù)資源訪問權(quán)限的不同,把內(nèi)存分為內(nèi)核空間和用戶空間,內(nèi)核空間的指令代碼具備直接調(diào)度計(jì)算機(jī)底層資源的能力(例如: CPU、I/O 資源等);用戶空間的代碼沒有訪問計(jì)算底層資源的能力,需要通過系統(tǒng)調(diào)用等方式切換為內(nèi)核態(tài)來實(shí)現(xiàn)對(duì)計(jì)算機(jī)底層資源的申請(qǐng)和調(diào)度。線程作為操作系統(tǒng)能夠調(diào)度的最小單位,也分為內(nèi)核級(jí)線程和用戶級(jí)線程:
- 內(nèi)核級(jí)線程由操作系統(tǒng)內(nèi)核創(chuàng)建和撤銷。內(nèi)核維護(hù)進(jìn)程及線程的上下文信息以及線程切換。一個(gè)內(nèi)核線程由于I/O操作而阻塞,不會(huì)影響其它線程的運(yùn)行。它能夠很好利用多核 CPU 并行計(jì)算的優(yōu)勢,開發(fā)人員可以通過系統(tǒng)調(diào)用使用內(nèi)核線程。
- 用戶級(jí)線程不需要內(nèi)核支持而在用戶程序中實(shí)現(xiàn)的線程,其不依賴于操作系統(tǒng)核心,應(yīng)用進(jìn)程利用線程庫提供創(chuàng)建、同步、調(diào)度和管理線程的函數(shù)來控制用戶線程。不需要用戶態(tài)/核心態(tài)切換,速度快,操作系統(tǒng)內(nèi)核不知道多線程的存在,因此一個(gè)線程阻塞將使得整個(gè)進(jìn)程(包括它的所有線程)阻塞。由于這里的處理器時(shí)間片分配是以進(jìn)程為基本單位,所以每個(gè)線程執(zhí)行的時(shí)間相對(duì)減少。我們一般情況下說的線程其實(shí)是指用戶級(jí)線程。
2.1 用戶級(jí)線程調(diào)度模型

用戶級(jí)線程調(diào)度模型中是一個(gè)進(jìn)程對(duì)應(yīng)一個(gè)內(nèi)核線程,進(jìn)程內(nèi)的可以創(chuàng)建多個(gè)用戶級(jí)線程,由于用戶級(jí)線程的創(chuàng)建、切換和同步等工作由用戶代碼完成,所以對(duì)用戶級(jí)線程的使用非常輕量和高效,但是這些復(fù)雜的管理邏輯需要在用戶代碼中實(shí)現(xiàn),一般依賴于編程語言層次。同時(shí)進(jìn)程內(nèi)的多用戶級(jí)線程無法很好利用 CPU 多核進(jìn)程的優(yōu)勢,只能通過分時(shí)復(fù)用的方式輪換執(zhí)行。當(dāng)進(jìn)程內(nèi)的任意用戶線程阻塞,很可能導(dǎo)致整個(gè)進(jìn)程的阻塞。
2.2 內(nèi)核級(jí)線程調(diào)度模型

內(nèi)核級(jí)線程模型中,進(jìn)程中的每個(gè)線程都會(huì)對(duì)應(yīng)一個(gè)內(nèi)核線程,進(jìn)程內(nèi)每創(chuàng)建一個(gè)新的線程都會(huì)調(diào)用操作系統(tǒng)的線程庫在內(nèi)核創(chuàng)建一個(gè)新的內(nèi)核線程與對(duì)應(yīng),線程的管理和調(diào)度有操作系統(tǒng)負(fù)責(zé),這將導(dǎo)致每次線程切換上下文時(shí)都會(huì)從用戶態(tài)切換到內(nèi)核態(tài),會(huì)有不小的資源消耗,同時(shí)創(chuàng)建線程的數(shù)量也會(huì)受制于操作系統(tǒng)內(nèi)核創(chuàng)建可創(chuàng)建的內(nèi)核線程數(shù)量。好處是多線程能夠充分利用 CPU 的多核并行計(jì)算能力,因?yàn)槊總€(gè)線程可以獨(dú)立被操作系統(tǒng)調(diào)度分配到 CPU 上執(zhí)行指令,同時(shí)某個(gè)線程的阻塞并不會(huì)影響到進(jìn)程內(nèi)其他線程工作的執(zhí)行。
2.3 兩級(jí)線程調(diào)度模型

兩級(jí)線程調(diào)度模型相當(dāng)于用戶級(jí)線程調(diào)度模型和內(nèi)核級(jí)線程調(diào)度模型的結(jié)合,一個(gè)進(jìn)程將會(huì)對(duì)應(yīng)多個(gè)內(nèi)核級(jí)線程,由進(jìn)程內(nèi)的調(diào)度器決定進(jìn)程內(nèi)的用戶級(jí)線程如何與申請(qǐng)的內(nèi)核級(jí)線程對(duì)應(yīng),進(jìn)程會(huì)預(yù)先申請(qǐng)一定數(shù)量的內(nèi)核級(jí)線程,然后將自身創(chuàng)建的用戶級(jí)線程與內(nèi)核級(jí)線程進(jìn)行對(duì)應(yīng)。線程的調(diào)用和管理由進(jìn)程內(nèi)的調(diào)度器進(jìn)行,而內(nèi)核線程的調(diào)度和管理由操作系統(tǒng)負(fù)責(zé)。這種線程調(diào)度模型即能夠有效降低線程創(chuàng)建和管理的資源消耗,也能夠很好提供線程并行計(jì)算的能力,但是給開發(fā)人員帶來較大的實(shí)現(xiàn)難度。
2.4 MPG調(diào)度模型

Go語言的MPG調(diào)度模型對(duì)兩級(jí)線程調(diào)度模型進(jìn)行一定程度的改進(jìn),使它能夠更加靈活地進(jìn)行線程之間的調(diào)度,其中:
M(Machine):一個(gè)M直接關(guān)聯(lián)了一個(gè)內(nèi)核級(jí)線程;
P(Processor):代表了M所需的上下文環(huán)境,是處理用戶級(jí)代碼邏輯的處理器;
G(Goroutine):本質(zhì)上是一種輕量級(jí)的線程。
注:P的個(gè)數(shù)是通過runtime.GOMAXPROCS設(shè)定的,從Go 1.5開始, Go的GOMAXPROCS默認(rèn)值已經(jīng)設(shè)置為CPU的核數(shù),這允許我們的Go程序充分使用機(jī)器的每一個(gè)CPU,最大程度的提高我們程序的并發(fā)性能,但其實(shí)對(duì)于IO密集型的場景,我們可以把GOMAXPROCS的值超過CPU核數(shù)。
調(diào)度流程:

- 當(dāng)一個(gè)Goroutine創(chuàng)建被創(chuàng)建時(shí),Goroutine對(duì)象被壓入Processor的本地隊(duì)列或者Go運(yùn)行時(shí)全局Goroutine隊(duì)列;
- Processor喚醒一個(gè)Machine,如果Machine的waiting隊(duì)列沒有等待被喚醒的Machine, 則創(chuàng)建一個(gè)(只要不超過Machine的最大值,10000),Processor獲取到Machine后,與此Machine綁定,并執(zhí)行此Goroutine;
- Machine執(zhí)行過程中,如果發(fā)生上下文切換,需要對(duì)執(zhí)行現(xiàn)場進(jìn)行保護(hù),以便下次被調(diào)度執(zhí)行時(shí)進(jìn)行現(xiàn)場恢復(fù)。Go調(diào)度器中Machine的棧保存在Goroutine對(duì)象上,只需要將Machine所需要的寄存器(堆棧指針、程序計(jì)數(shù)器等)保存到Goroutine對(duì)象上 即可;如果此時(shí)Goroutine任務(wù)還沒有執(zhí)行完,Machine可以將Goroutine重新壓入Processor的隊(duì)列,等待下一次被調(diào)度執(zhí)行。如果執(zhí)行過程遇到阻塞并阻塞超時(shí),Machine會(huì)與Processor分離,并等待阻塞結(jié)束。此時(shí) Processor可以繼續(xù)喚醒Machine執(zhí)行其它的Goroutine,當(dāng)阻塞結(jié)束時(shí),Machine會(huì)嘗試” 偷取”一個(gè)Processor,如果失敗,這個(gè)Goroutine會(huì)被加入到全局隊(duì)列中,然后Machine將 自己轉(zhuǎn)入Waiting隊(duì)列,等待被再次喚醒。
3 總結(jié)
- Go語言以CSP并發(fā)模型為并發(fā)基礎(chǔ),使用非常輕量的協(xié)程(Goroutine)做為并發(fā)實(shí)體,對(duì)協(xié)程(Goroutine)的調(diào)度是在用戶態(tài)下完成的, 不涉及內(nèi)核態(tài)與用戶態(tài)之間的頻繁切換;
- 協(xié)程(Goroutine)間通過管道(Channel)實(shí)現(xiàn)高效通信,實(shí)現(xiàn)了并發(fā)實(shí)體間的解耦;
- Go在語言層面實(shí)現(xiàn)了自動(dòng)調(diào)度,這樣屏蔽了很多內(nèi)部細(xì)節(jié),對(duì)外提供簡單的語法關(guān)鍵字,大大簡化了并發(fā)編程的思維轉(zhuǎn)換和管理線程的復(fù)雜性。