你很可能從某種途徑聽說過 Go 語言。它越來越受歡迎,並且有充分的理由可以證明。 Go 快速、簡單,有強大的社區支援。學習這門語言最令人興奮的一點是它的並行存取模型。 Go 的並發原語使建立多線程並發程式變得簡單而有趣。我將通過插圖介紹 Go 的並發原語,希望能點透相關概念以方便後續學習。本文是寫給 Go 語言編程新手以及準備開始學習 Go 並發原語 (goroutines 和 channels) 的同學。## 單線程程式 vs. 多線程程式你可能已經寫過一些單線程程式。一個常用的編程模式是組合多個函數來執行一個特定任務,並且只有前一個函數準備好資料,後面的才會被調用。![single Gopher](https://raw.githubusercontent.com/studygolang/gctt-images/master/Learning-Go-s-Concurrency-Through-Illustrations/single-gopher.jpeg)首先我們將用上述模式編寫第一個例子的代碼,一個描述挖礦的程式。它包含三個函數,分別負責執行尋礦、挖礦和練礦任務。在本例中,我們用一組字串表示 `rock`(礦山) 和 `ore`(礦石),每個函數都以它們作為輸入,並返回一組 “處理過的” 字串。對於一個單線程的應用而言,該程式可能會按如下方式來設計:![ore mining single-threaded program](https://raw.githubusercontent.com/studygolang/gctt-images/master/Learning-Go-s-Concurrency-Through-Illustrations/ore-mining-single-threaded-program.jpeg)它有三個主要的函數:*finder*、*miner* 和 *smelter*。該版本的程式的所有函數都在單一線程中運行,一個接著一個執行,並且這個線程 (名為 Gary 的 gopher) 需要處理全部工作。```gofunc main() {theMine := [5]string{"rock", "ore", "ore", "rock", "ore"}foundOre := finder(theMine)minedOre := miner(foundOre)smelter(minedOre)}```在每個函數最後列印出 "ore" 處理後的結果,得到如下輸出:```From Finder: [ore ore ore]From Miner: [minedOre minedOre minedOre]From Smelter: [smeltedOre smeltedOre smeltedOre]```這種編程風格具有易於設計的優點,但是當你想利用多個線程並執行彼此獨立的函數時會發生什麼呢?這就是並發程式設計發揮作用的地方。![ore mining concurrent program](https://raw.githubusercontent.com/studygolang/gctt-images/master/Learning-Go-s-Concurrency-Through-Illustrations/ore-mining-concurrent-program.jpeg)這種設計使得 “挖礦” 更高效。現在多個線程 (gophers) 是獨立啟動並執行,從而 Gary 不再承擔全部工作。其中一個 gopher 負責尋礦,一個負責挖礦,另一個負責練礦,這些工作可能同時進行。為了將這種並發特性引入我們的代碼,我們需要建立獨立啟動並執行 gophers 的方法以及它們之間彼此通訊 (傳送礦石) 的方法。這就需要用到 Go 的並發原語:goroutines 和 channels。## GoroutinesGoroutines 可以看作是輕量級線程。建立一個 goroutine 非常簡單,只需要把 *go* 關鍵字放在函數調用語句前。為了說明這有多麼簡單,我們建立兩個 finder 函數,並用 *go* 調用,讓它們每次找到 "ore" 就列印出來。![go myFunc()](https://raw.githubusercontent.com/studygolang/gctt-images/master/Learning-Go-s-Concurrency-Through-Illustrations/go.jpeg)```gofunc main() {theMine := [5]string{"rock", "ore", "ore", "rock", "ore"}go finder1(theMine)go finder2(theMine)<-time.After(time.Second * 5) //you can ignore this for now}```程式的輸出如下:```Finder 1 found ore!Finder 2 found ore!Finder 1 found ore!Finder 1 found ore!Finder 2 found ore!Finder 2 found ore!```可以看出,兩個 finder 是並發啟動並執行。哪一個先找到礦石沒有確定的順序,當執行多次程式時,這個順序並不總是相同的。這是一個很大的進步!現在我們有一個簡單的方法來建立多線程 (multi-gopher) 程式,但是當我們需要獨立的 goroutines 之間彼此通訊會發生什麼呢?歡迎來到神奇的 *channels* 世界。## Channels![communication](https://raw.githubusercontent.com/studygolang/gctt-images/master/Learning-Go-s-Concurrency-Through-Illustrations/communication.jpeg)Channels 允許 go routines 之間相互連信。你可以把 channel 看作管道,goroutines 可以往裡面發訊息,也可以從中接收其它 go routines 的訊息。![my first channel](https://raw.githubusercontent.com/studygolang/gctt-images/master/Learning-Go-s-Concurrency-Through-Illustrations/channel.jpeg)```gomyFirstChannel := make(chan string)```Goroutines 可以往 channel 發送訊息,也可以從中接收訊息。這是通過箭頭操作符 (<-) 完成的,它指示 channel 中的資料流向。![arrow](https://raw.githubusercontent.com/studygolang/gctt-images/master/Learning-Go-s-Concurrency-Through-Illustrations/channel-arrow.jpeg)```gomyFirstChannel <-"hello" // SendmyVariable := <- myFirstChannel // Receive```現在通過 channel 我們可以讓尋礦 gopher 一找到礦石就立即傳送給開礦 gopher ,而不用等發現所有礦石。![ore channel](https://raw.githubusercontent.com/studygolang/gctt-images/master/Learning-Go-s-Concurrency-Through-Illustrations/ore-channel.jpeg)我重寫了挖礦程式,把尋礦和開礦函數改寫成了未命名函數。如果你從未見過 匿名函式,不必過多關注這部分,只需要知道每個函數將通過 *go* 關鍵字調用並運行在各自的 goroutine 中。重要的是,要注意 goroutine 之間是如何通過 channel ```oreChan``` 傳遞資料的。別擔心,我會在最後面解釋未命名函數的。```gofunc main() {theMine := [5]string{"ore1", "ore2", "ore3"}oreChan := make(chan string)// Findergo func(mine [5]string) {for _, item := range mine {oreChan <- item //send}}(theMine)// Ore Breakergo func() {for i := 0; i < 3; i++ {foundOre := <-oreChan //receivefmt.Println("Miner: Received " + foundOre + " from finder")}}()<-time.After(time.Second * 5) // Again, ignore this for now}```從下面的輸出,可以看到 Miner 從 `oreChan` 讀取了三次,每次接收一塊礦石。```Miner: Received ore1 from finderMiner: Received ore2 from finderMiner: Received ore3 from finder```太棒了,現在我們能在程式的 goroutines(gophers) 之間發送資料了。在開始用 channels 寫複雜的程式之前,我們先來理解它的一些關鍵特性。### Channel BlockingChannels 阻塞 goroutines 發生在各種情形下。這能在 goroutines 各自歡快地運行之前,實現彼此之間的短暫同步。### Blocking on a Send![blocking on send](https://raw.githubusercontent.com/studygolang/gctt-images/master/Learning-Go-s-Concurrency-Through-Illustrations/blocking-on-send.jpeg)一旦一個 goroutine(gopher) 向一個 channel 發送資料,它就被阻塞了,直到另一個 goroutine 從該 channel 取走資料。### Blocking on a Receive![blocking on receive](https://raw.githubusercontent.com/studygolang/gctt-images/master/Learning-Go-s-Concurrency-Through-Illustrations/blocking-on-receive.jpeg)和發送時情形類似,一個 goroutine 可能阻塞著等待從一個 channel 擷取資料,如果還沒有其他 goroutine 往該 channel 發送資料。一開始接觸阻塞的概念可能令人有些困惑,但你可以把它想象成兩個 goroutines(gophers) 之間的交易。 其中一個 gopher 無論是等著收錢還是送錢,都需要等待交易的另一方出現。既然已經瞭解 goroutine 通過 channel 通訊可能發生阻塞的不同情形,讓我們討論兩種不同類型的 channels: *unbuffered* 和 *buffered* 。選擇使用哪一種 channel 可能會改變程式的運行表現。### Unbuffered Channels![unbuffered channel](https://raw.githubusercontent.com/studygolang/gctt-images/master/Learning-Go-s-Concurrency-Through-Illustrations/unbuffered-channel.jpeg)在前面的例子中我們一直在用 unbuffered channels,它們與眾不同的地方在於每次只有一份資料可以通過。### Buffered Channels![buffered channel](https://raw.githubusercontent.com/studygolang/gctt-images/master/Learning-Go-s-Concurrency-Through-Illustrations/buffered-channel.jpeg)在並發程式中,時間協調並不總是完美的。在挖礦的例子中,我們可能遇到這樣的情形:開礦 gopher 處理一塊礦石所花的時間,尋礦 gohper 可能已經找到 3 塊礦石了。為了不讓尋礦 gopher 浪費大量時間等著給開礦 gopher 傳送礦石,我們可以使用 *buffered* channel。我們先建立一個容量為 3 的 buffered channel。```gobufferedChan := make(chan string, 3)```buffered 和 unbuffered channels 工作原理類似,但有一點不同—在需要另一個 gorountine 取走資料之前,我們可以向 buffered channel 發送多份資料。![cap 3 buffered channel](https://raw.githubusercontent.com/studygolang/gctt-images/master/Learning-Go-s-Concurrency-Through-Illustrations/cap-3-buffered-channel.jpeg)```gobufferedChan := make(chan string, 3)go func() {bufferedChan <-"first"fmt.Println("Sent 1st")bufferedChan <-"second"fmt.Println("Sent 2nd")bufferedChan <-"third"fmt.Println("Sent 3rd")}()<-time.After(time.Second * 1)go func() {firstRead := <- bufferedChanfmt.Println("Receiving..")fmt.Println(firstRead)secondRead := <- bufferedChanfmt.Println(secondRead)thirdRead := <- bufferedChanfmt.Println(thirdRead)}()```兩個 goroutines 之間的列印順序如下:```Sent 1stSent 2ndSent 3rdReceiving..firstsecondthird```為了簡單起見,我們在最終的程式中不使用 buffered channels。但知道該使用哪種 channel 是很重要的。> 注意: 使用 buffered channels 並不會避免阻塞發生。例如,如果尋礦 gopher 比開礦 gopher 執行速度快 10 倍,並且它們通過一個容量為 2 的 buffered channel 進行通訊,那麼尋礦 gopher 仍會發生多次阻塞。## 把這些都放到一起現在憑藉 goroutines 和 channels 的強大功能,我們可以使用 Go 的並發原語編寫一個充分發揮多線程優勢的程式了。![putting it all together](https://raw.githubusercontent.com/studygolang/gctt-images/master/Learning-Go-s-Concurrency-Through-Illustrations/all-together.jpeg)```gotheMine := [5]string{"rock", "ore", "ore", "rock", "ore"}oreChannel := make(chan string)minedOreChan := make(chan string)// Findergo func(mine [5]string) {for _, item := range mine {if item == "ore" {oreChannel <- item //send item on oreChannel}}}(theMine)// Ore Breakergo func() {for i := 0; i < 3; i++ {foundOre := <-oreChannel //read from oreChannelfmt.Println("From Finder:", foundOre)minedOreChan <-"minedOre" //send to minedOreChan}}()// Smeltergo func() {for i := 0; i < 3; i++ {minedOre := <-minedOreChan //read from minedOreChanfmt.Println("From Miner:", minedOre)fmt.Println("From Smelter: Ore is smelted")}}()<-time.After(time.Second * 5) // Again, you can ignore this```程式輸出如下:```From Finder: oreFrom Finder: oreFrom Miner: minedOreFrom Smelter: Ore is smeltedFrom Miner: minedOreFrom Smelter: Ore is smeltedFrom Finder: oreFrom Miner: minedOreFrom Smelter: Ore is smelted```相比最初的例子,已經有了很大改進!現在每個函數都獨立地運行在各自的 goroutines 中。此外,每次處理完一塊礦石,它就會被帶進挖礦流水線的下一個階段。為了專註於理解 goroutines 和 channel 的基本概念,上文有些重要的資訊我沒有提,如果不知道的話,當你開始編程時它們可能會造成一些麻煩。既然你已經理解了 goroutines 和 channel 的工作原理,在開始用它們編寫代碼之前,讓我們先瞭解一些你應該知道的其他資訊。## 在開始之前,你應該知道...### 匿名的 Goroutines![anonymous goroutine](https://raw.githubusercontent.com/studygolang/gctt-images/master/Learning-Go-s-Concurrency-Through-Illustrations/anonymous-go-routine.jpeg)類似於如何利用 *go* 關鍵字使一個函數運行在自己的 goroutine 中,我們可以用如下方式建立一個匿名函數並運行在它的 goroutine 中:```go// Anonymous go routinego func() {fmt.Println("I'm running in my own go routine")}()```如果只需要調用一次函數,通過這種方式我們可以讓它在自己的 goroutine 中運行,而不需要建立一個正式的函式宣告。### main 函數是一個 goroutine![main func](https://raw.githubusercontent.com/studygolang/gctt-images/master/Learning-Go-s-Concurrency-Through-Illustrations/main-func.jpeg)main 函數確實運行在自己的 goroutine 中!更重要的是要知道,一旦 main 函數返回,它將關掉當前正在啟動並執行其他 goroutines。這就是為什麼我們在 main 函數的最後設定了一個定時器—它建立了一個 channel,並在 5 秒後發送一個值。```go<-time.After(time.Second * 5) // Receiving from channel after 5 sec```還記得 goroutine 從 channel 中讀資料如何被阻塞直到有資料發送到裡面吧?通過添加上面這行代碼,main routine 將會發生這種情況。它會阻塞,以給其他 goroutines 5 秒的時間來運行。現在有更好的方式阻塞 main 函數直到其他所有 goroutines 都運行完。通常的做法是建立一個 *done channel*, main 函數在等待讀取它時被阻塞。一旦完成工作,向這個 channel 發送資料,程式就會結束了。![done chan](https://raw.githubusercontent.com/studygolang/gctt-images/master/Learning-Go-s-Concurrency-Through-Illustrations/done.jpeg)```gofunc main() {doneChan := make(chan string)go func() {// Do some work…doneChan <- "I'm all done!"}()<-doneChan // block until go routine signals work is done}```### 你可以遍曆 channel在前面的例子中我們讓 miner 在 for 迴圈中迭代 3 次從 channel 中讀取資料。如果我們不能確切知道將從 finder 接收多少塊礦石呢?好吧,類似於對集合資料類型 (注: 如 slice) 進行遍曆,你也可以遍曆一個 channel。更新前面的 miner 函數,我們可以這樣寫:```go// Ore Breakergo func() {for foundOre := range oreChan {fmt.Println("Miner: Received " + foundOre + " from finder")}}()```由於 miner 需要讀取 finder 發送給它的所有資料,遍曆 channel 能確保我們接收到已經發送的所有資料。> 遍曆 channel 會阻塞,直到有新資料被發送到 channel。在所有資料發送完之後避免 go routine 阻塞的唯一方法就是用 "close(channel)" 關掉 channel。### 對 channel 進行非阻塞讀但你剛剛告訴我們 channel 如何阻塞 goroutine 的各種情形?!沒錯,不過還有一個技巧,利用 Go 的 *select case* 語句可以實現對 channel 的非阻塞讀。通過使用這這種語句,如果 channel 有資料,goroutine 將會從中讀取,否則就執行預設的分支。```gomyChan := make(chan string)go func(){myChan <- "Message!"}()select {case msg := <- myChan:fmt.Println(msg)default:fmt.Println("No Msg")}<-time.After(time.Second * 1)select {case msg := <- myChan:fmt.Println(msg)default:fmt.Println("No Msg")}```程式輸出如下:```No MsgMessage!```### 對 channel 進行非阻塞寫非阻塞寫也是使用同樣的 *select case* 語句來實現,唯一不同的地方在於,case 語句看起來像是發送而不是接收。```goselect {case myChan <- "message":fmt.Println("sent the message")default:fmt.Println("no message sent")}```## 接下來去哪兒學![where go](https://raw.githubusercontent.com/studygolang/gctt-images/master/Learning-Go-s-Concurrency-Through-Illustrations/where-go.jpeg)有許多講座和部落格更詳細地介紹了 channels 和 goroutines。 既然已經對這些工具的目的和應用有了深刻的理解,那麼你應該能夠充分利用下面的文章和演講了。> [*Google I/O 2012 — Go Concurrency Patterns*](https://www.youtube.com/watch?v=f6kdp27TYZs&t=938s)>> [*Rob Pike — 'Concurrency Is Not Parallelism'*](https://www.youtube.com/watch?v=cN_DpYBzKso)>> [*GopherCon 2017: Edward Muller — Go Anti-Patterns*](https://www.youtube.com/watch?v=ltqV6pDKZD8&t=1315s)謝謝您花時間閱讀本文。我希望你能夠理解 goroutines 和 channels 基本概念,以及使用它們給編寫並發程式帶來的好處。
via: https://medium.com/@trevor4e/learning-gos-concurrency-through-illustrations-8c4aff603b3
作者:Trevor Forrey 譯者:mbyd916 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
1251 次點擊 ∙ 1 贊