關(guān)于Go語言,你不得不知的并發(fā)模式!
點擊上方“Go編程時光”,選擇“加為星標”
第一時間關(guān)注Go技術(shù)干貨!

什么是并發(fā)?有哪些我們需要知道的并發(fā)模式?Go語言中的協(xié)程并發(fā)模型是怎樣的?什么是主 goroutine?它與我們自己啟用的其他goroutine 有什么不同?
本文就來為你一一解答!
以下內(nèi)容節(jié)選自《Go語言極簡一本通:零基礎(chǔ)入門到項目實戰(zhàn)》一書!

▊ 并發(fā)
串行程序,即程序的執(zhí)行順序和程序的編寫順序一致,整個程序只有一個上下文,就是一個棧,一個堆。
并發(fā)程序,則需要運行多個上下文,對應(yīng)多個調(diào)用棧。每個進程在運行時,都有自己的調(diào)用棧和堆,有一套完整的上下文。操作系統(tǒng)在調(diào)用時,會保證被調(diào)度進程的上下文環(huán)境,待該進程獲得時間后,再將該進程的上下文恢復(fù)到系統(tǒng)中。
串行的代碼是逐行執(zhí)行的,是確定的,而并發(fā)引入了不確定性。線程通信只能采用共享內(nèi)存的方式,為了保證共享內(nèi)存的有效性,可以加鎖,但是這樣又引入了死鎖的風(fēng)險。
并發(fā)的優(yōu)勢如下:
(1)可以充分利用CPU 核心的優(yōu)勢,提高程序的執(zhí)行效率。
(2)并發(fā)能充分利用CPU 與其他硬件設(shè)備的異步性,如文件操作等。
下面介紹3種并發(fā)模式。
1.多進程是操作系統(tǒng)層面的并發(fā)模式
所有的進程都由內(nèi)核管理。進程描述的是程序的執(zhí)行過程,是運行著的程序。
一個進程其實就是一個程序運行時的產(chǎn)物。
電腦為什么可以同時運行那么多應(yīng)用程序?手機為什么可以有那么多App 同時在后臺刷新?
這是因為在它們的操作系統(tǒng)之上有多個代表著不同應(yīng)用程序的進程在同時運行。
操作系統(tǒng)會為每個獨立的程序創(chuàng)建一個進程,進程可以裝下整個程序需要的資源。例如,程序執(zhí)行的進度、執(zhí)行的結(jié)果等,都可以放在里面。在程序運行結(jié)束后,再把進程銷毀,然后運行下一個程序,周而復(fù)始。
進程在程序運行中是非常占用資源的,無論是否會用到全部的資源,只要程序啟動了,就會被加載到進程中。
優(yōu)勢是進程互不影響,劣勢是開銷非常大。
2.多線程屬于系統(tǒng)層面的并發(fā)模式,也是使用最多、最有效的一種模式
線程是在進程之內(nèi)的,可以把它理解為輕量級的進程。它可以被視為進程中代碼的執(zhí)行流程。這樣在處理程序的運行和記錄中間結(jié)果時,就可以使用更少的資源。待資源用完,線程就會被銷毀。
線程要比進程輕量級很多。一個進程至少包含一個線程。如果一個進程只包含一個線程,那么它里面的所有代碼都只會被串行地執(zhí)行。
每個進程的第一個線程都會隨著該進程的啟動而被創(chuàng)建,它們被稱為其所屬進程的主線程。同理,如果一個進程中包含多個線程,那么其中的代碼就可以被并發(fā)地執(zhí)行。
除進程的第一個線程外,其他的線程都是由進程中已存在的線程創(chuàng)建出來的。也就是說,主線程之外的其他線程都只能由代碼顯式地創(chuàng)建和銷毀。這需要我們在編寫程序時進行手動控制。
優(yōu)勢是比進程開銷小一些,劣勢是開銷仍然較大。
3.goroutine
從本質(zhì)上說,goroutine 是一種用戶態(tài)線程,不需要操作系統(tǒng)進行搶占式調(diào)度。
在Go 程序中,Go 語言的運行時系統(tǒng)會自動地創(chuàng)建和銷毀系統(tǒng)級的線程。
系統(tǒng)級線程指的是操作系統(tǒng)提供的線程,而對應(yīng)的用戶級線程(goroutine)指的是架設(shè)在系統(tǒng)級線程之上的,由用戶(或者說我們編寫的程序)完全控制的代碼執(zhí)行流程。
用戶級線程的創(chuàng)建、銷毀、調(diào)度、狀態(tài)變更,以及其中的代碼和數(shù)據(jù)都完全需要我們的程序自己去實現(xiàn)和處理,其優(yōu)勢如下:
(1)因為它們的創(chuàng)建和銷毀不需要通過操作系統(tǒng)去做,所以速度很快,可以提高任務(wù)并發(fā)性。編程簡單、結(jié)構(gòu)清晰。
(2)由于不用操作系統(tǒng)去調(diào)度它們的運行,所以很容易控制,并且很靈活。
▊ 協(xié)程并發(fā)模型
在Go 語言中,不要通過共享數(shù)據(jù)來通信,恰恰相反,要通過通信的方式來共享數(shù)據(jù)。
Go 語言不僅有g(shù)oroutine,還有強大的用來調(diào)度 goroutine、對接系統(tǒng)級線程的調(diào)度器。
調(diào)度器是 Go 語言運行時系統(tǒng)的重要組成部分,它主要負責(zé)統(tǒng)籌調(diào)配 Go 并發(fā)編程模型中的三個主要元素,即G(goroutine 的縮寫)、P(processor 的縮寫)和 M(machine 的縮寫),如下圖所示。

其中,M 指的就是系統(tǒng)級線程。而P 指的是一種可以引用若干個G,且能夠使這些G 在恰當?shù)臅r機與M 進行對接,并得到運行的中介。
從宏觀上說,由于P 的存在,G 和M 可以呈現(xiàn)出多對多的關(guān)系。當一個正在與某個M 對接并運行著的G,需要因某個事件(比如等待 I/O 或鎖的解除)而暫停運行時,調(diào)度器總會及時地發(fā)現(xiàn),并把這個G 與那個M 分離開,以釋放計算資源供那些等待運行的G 使用。
而當一個G 需要恢復(fù)運行時,調(diào)度器又會盡快地為它尋找空閑的計算資源(包括M)并安排運行。另外,當M 不夠用時,調(diào)度器會向操作系統(tǒng)申請新的系統(tǒng)級線程,而當某個M 已無用時,調(diào)度器又會負責(zé)把它及時地銷毀。
程序中的所有 goroutine 也都會被充分地調(diào)度,其中的代碼也都會被并發(fā)地運行,即使goroutine 數(shù)以十萬計,仍然可以如此。
什么是主 goroutine?它與我們自己啟用的其他goroutine 有什么不同?
先來看下面的代碼:
package mainimport "fmt"func main() {for i := 0; i < 10; i++ {go func() {fmt.Println(i)}()}}
這段代碼只在main 函數(shù)中寫了一條for 語句。這條for 語句中的代碼會迭代運行10 次,并有一個局部變量i 表示當次迭代的序號,該序號是從0 開始的。在這條for 語句中僅有一條Go語句,在這條Go 語句中也僅有一條語句,該語句調(diào)用了fmt.Println 函數(shù),想要打印出變量i 的值。
這個程序很簡單,只有三條語句。這個程序被執(zhí)行后,會打印出什么內(nèi)容呢?
答案是:大部分計算機執(zhí)行后,屏幕上不會有任何內(nèi)容被打印出來。
這是為什么呢?
一個進程總會有一個主線程,類似地,每一個獨立的Go 程序在運行時也總會有一個主goroutine。這個主goroutine 會在Go 程序的運行準備工作完成后被自動地啟用。
一般來說,每條Go 語句都帶有一個函數(shù)調(diào)用,這個被調(diào)用的函數(shù)就是Go 函數(shù)。而主goroutine 的Go 函數(shù)就是那個作為程序入口的main 函數(shù)。Go 函數(shù)執(zhí)行的時間與其所屬的Go語句執(zhí)行的時間不同。
如下圖所示,當程序執(zhí)行到一條Go 語句時,Go 語言的運行時系統(tǒng)會先試圖從某個空閑的G 隊列中獲取一個G(也就是goroutine),只有在找不到空閑G 的情況下它才會去創(chuàng)建一個新的G。

如果已經(jīng)存在一個goroutine,那么已存在的goroutine 總是會被優(yōu)先復(fù)用。如果不存在,就去啟動另一個goroutine。
在Go 語言中,創(chuàng)建G 的成本非常低。創(chuàng)建一個G 并不需要像新建一個進程或者一個系統(tǒng)級線程那樣,必須通過操作系統(tǒng)的系統(tǒng)調(diào)用來完成,而是在 Go 語言的運行時系統(tǒng)內(nèi)部就可以完全做到,一個G 僅相當于為需要并發(fā)執(zhí)行代碼片段服務(wù)的上下文環(huán)境。
在拿到一個空閑的G 之后,Go 語言運行時系統(tǒng)會用這個G 去包裝當前的那個Go 函數(shù)(或者一個匿名的函數(shù)),然后再把這個G 追加到某個可運行的G 隊列中。隊列中的G 總是按照先入先出的順序,由運行時系統(tǒng)安排運行。
由于上面所說的那些準備工作是不可避免的,所以會消耗一定時間。因此,Go 函數(shù)的執(zhí)行時間總是慢于它所屬的Go 語句的執(zhí)行時間。
明白了這些之后,再來看上面的例子。請記住,只要Go 語句本身執(zhí)行完畢,Go 程序不會等待Go 函數(shù)的執(zhí)行,它就會立刻執(zhí)行后邊的語句,這就是異步并發(fā)執(zhí)行。
這里“后邊的語句”一般指的是上面例子中 for 語句中的下一個迭代。當最后一個迭代運行時,這個“后邊的語句”是不存在的。
上面的那條for 語句會以很快的速度執(zhí)行完畢。當它執(zhí)行完畢時,那10 個包裝了Go 函數(shù)的 goroutine 往往還沒有獲得運行的機會。Go 函數(shù)中的那個對fmt.Println 函數(shù)的調(diào)用是以for 語句中的變量i 作為參數(shù)的。
當for 語句執(zhí)行完畢時,這些Go 函數(shù)都還沒有執(zhí)行,那么它們引用的變量i 是多少呢?
一旦主 goroutine 中的代碼(也就是main 函數(shù)中的那些代碼)執(zhí)行完畢,當前的Go 程序就會結(jié)束運行。當Go 程序結(jié)束運行時,無論其他的goroutine 是否運行,都不會被執(zhí)行了。當for語句的最后一個迭代運行時,其中的那條Go 語句即最后一條語句。所以,在執(zhí)行完這條Go語句之后,主goroutine 中的代碼就執(zhí)行完了,Go 程序會立即結(jié)束運行。因此前面的代碼不會有任何內(nèi)容被打印輸出。
嚴謹?shù)刂v,Go 語言并不管這些goroutine 以怎樣的順序運行。由于主goroutine 會與我們自己啟用的其他 goroutine 一起被調(diào)度,而調(diào)度器很可能會在goroutine 中的代碼只執(zhí)行了一部分的時候暫停,以便所有的goroutine 都有運行的機會。所以哪個goroutine 先執(zhí)行完,哪個goroutine后執(zhí)行完往往是不可預(yù)知的。
對于上面簡單的代碼而言,絕大多數(shù)情況都是“不會有任何內(nèi)容被打印出來”。但是為了嚴謹起見,無論回答“打印出 10 個10”,還是“不會有任何內(nèi)容被打印出來”,或是“打印出亂序的0 到9”都是對的。
這個原理非常重要,希望讀者能理解。


▊《Go語言極簡一本通:零基礎(chǔ)入門到項目實戰(zhàn)》
歡喜 編著
一本就通,一學(xué)就會!
本書是一本Go語言入門書,全書共分為三部分。第一部分講解Go語言基礎(chǔ)知識,包括變量與簡單類型、數(shù)組、切片、流程控制、字典、函數(shù)、結(jié)構(gòu)體與方法、接口等,可以幫助讀者快速掌握Go語言的基本程序結(jié)構(gòu)。第二部分講解Go語言高效并發(fā)相關(guān)知識,包括協(xié)程與通道、并發(fā)資源、包管理和測試等,讓讀者對Go語言層面的并發(fā)支持有更深入的理解。第三部分講解Go語言項目實戰(zhàn),包括Gin框架、生活點評項目實戰(zhàn)、賬戶管理系統(tǒng)實戰(zhàn),以及OAuth 2.0的授權(quán)協(xié)議等。通過實戰(zhàn),把前面講解的知識點運用起來,幫助讀者快速上手,積累項目經(jīng)驗。
(掃碼了解本書詳情)
看了上面的一些介紹,若覺得內(nèi)容適合你當下的水平,可以掃上面的二維碼進行購買。
過去這段時間,明哥這邊也接到了一些商業(yè)推廣,感謝大家一直以來的關(guān)注與包容,明哥無以回報,這次也借這個新書發(fā)布的機會,向出版社要了 5 本書,來送給大家。
很遺憾,這個號并沒有留言功能,因此我組建了一個群,想要抽書的朋友,可以掃下面的二維碼加下我的微信(記得備注:抽獎),我今晚 20 點會在群里發(fā)抽獎鏈接。
長按三秒,添加我好友 備注:抽獎
