你極可能從某種途徑據說過 Go 語言。它愈來愈受歡迎,而且有充分的理由能夠證實。 Go 快速、簡單,有強大的社區支持。學習這門語言最使人興奮的一點是它的併發模型。 Go 的併發原語使建立多線程併發程序變得簡單而有趣。我將經過插圖介紹 Go 的併發原語,但願能點透相關概念以方便後續學習。本文是寫給 Go 語言編程新手以及準備開始學習 Go 併發原語 (goroutines 和 channels) 的同窗。編程
你可能已經寫過一些單線程程序。一個經常使用的編程模式是組合多個函數來執行一個特定任務,而且只有前一個函數準備好數據,後面的纔會被調用。多線程
首先咱們將用上述模式編寫第一個例子的代碼,一個描述挖礦的程序。它包含三個函數,分別負責執行尋礦、挖礦和練礦任務。在本例中,咱們用一組字符串表示 rock
(礦山) 和 ore
(礦石),每一個函數都以它們做爲輸入,並返回一組 「處理過的」 字符串。對於一個單線程的應用而言,該程序可能會按以下方式來設計:併發
它有三個主要的函數:finder、miner 和 smelter。該版本的程序的全部函數都在單一線程中運行,一個接着一個執行,而且這個線程 (名爲 Gary 的 gopher) 須要處理所有工做。函數
func 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]
這種編程風格具備易於設計的優勢,可是當你想利用多個線程並執行彼此獨立的函數時會發生什麼呢?這就是併發程序設計發揮做用的地方。ui
這種設計使得 「挖礦」 更高效。如今多個線程 (gophers) 是獨立運行的,從而 Gary 再也不承擔所有工做。其中一個 gopher 負責尋礦,一個負責挖礦,另外一個負責練礦,這些工做可能同時進行。this
爲了將這種併發特性引入咱們的代碼,咱們須要建立獨立運行的 gophers 的方法以及它們之間彼此通訊 (傳送礦石) 的方法。這就須要用到 Go 的併發原語:goroutines 和 channels。spa
Goroutines 能夠看做是輕量級線程。建立一個 goroutine 很是簡單,只須要把 go 關鍵字放在函數調用語句前。爲了說明這有多麼簡單,咱們建立兩個 finder 函數,並用 go 調用,讓它們每次找到 "ore" 就打印出來。線程
func 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 容許 go routines 之間相互通訊。你能夠把 channel 看做管道,goroutines 能夠往裏面發消息,也能夠從中接收其它 go routines 的消息。
myFirstChannel := make(chan string)
Goroutines 能夠往 channel 發送消息,也能夠從中接收消息。這是經過箭頭操做符 (<-) 完成的,它指示 channel 中的數據流向。
myFirstChannel <-"hello" // Send myVariable := <- myFirstChannel // Receive
如今經過 channel 咱們可讓尋礦 gopher 一找到礦石就當即傳送給開礦 gopher ,而不用等發現全部礦石。
我重寫了挖礦程序,把尋礦和開礦函數改寫成了未命名函數。若是你從未見過 lambda 函數,沒必要過多關注這部分,只須要知道每一個函數將經過 go 關鍵字調用並運行在各自的 goroutine 中。重要的是,要注意 goroutine 之間是如何經過 channel oreChan
傳遞數據的。別擔憂,我會在最後面解釋未命名函數的。
func main() { theMine := [5]string{"ore1", "ore2", "ore3"} oreChan := make(chan string) // Finder go func(mine [5]string) { for _, item := range mine { oreChan <- item //send } }(theMine) // Ore Breaker go func() { for i := 0; i < 3; i++ { foundOre := <-oreChan //receive fmt.Println("Miner: Received " + foundOre + " from finder") } }() <-time.After(time.Second * 5) // Again, ignore this for now }
從下面的輸出,能夠看到 Miner 從 oreChan
讀取了三次,每次接收一塊礦石。
Miner: Received ore1 from finder Miner: Received ore2 from finder Miner: Received ore3 from finder
太棒了,如今咱們能在程序的 goroutines(gophers) 之間發送數據了。在開始用 channels 寫複雜的程序以前,咱們先來理解它的一些關鍵特性。
Channels 阻塞 goroutines 發生在各類情形下。這能在 goroutines 各自歡快地運行以前,實現彼此之間的短暫同步。
一旦一個 goroutine(gopher) 向一個 channel 發送數據,它就被阻塞了,直到另外一個 goroutine 從該 channel 取走數據。
和發送時情形相似,一個 goroutine 可能阻塞着等待從一個 channel 獲取數據,若是尚未其餘 goroutine 往該 channel 發送數據。
一開始接觸阻塞的概念可能使人有些困惑,但你能夠把它想象成兩個 goroutines(gophers) 之間的交易。 其中一個 gopher 不管是等着收錢仍是送錢,都須要等待交易的另外一方出現。
既然已經瞭解 goroutine 經過 channel 通訊可能發生阻塞的不一樣情形,讓咱們討論兩種不一樣類型的 channels: unbuffered 和 buffered 。選擇使用哪種 channel 可能會改變程序的運行表現。
在前面的例子中咱們一直在用 unbuffered channels,它們不同凡響的地方在於每次只有一份數據能夠經過。
在併發程序中,時間協調並不老是完美的。在挖礦的例子中,咱們可能遇到這樣的情形:開礦 gopher 處理一塊礦石所花的時間,尋礦 gohper 可能已經找到 3 塊礦石了。爲了避免讓尋礦 gopher 浪費大量時間等着給開礦 gopher 傳送礦石,咱們可使用 buffered channel。咱們先建立一個容量爲 3 的 buffered channel。
bufferedChan := make(chan string, 3)
buffered 和 unbuffered channels 工做原理相似,但有一點不一樣—在須要另外一個 gorountine 取走數據以前,咱們能夠向 buffered channel 發送多份數據。
bufferedChan := 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 := <- bufferedChan fmt.Println("Receiving..") fmt.Println(firstRead) secondRead := <- bufferedChan fmt.Println(secondRead) thirdRead := <- bufferedChan fmt.Println(thirdRead) }()
兩個 goroutines 之間的打印順序以下:
Sent 1st Sent 2nd Sent 3rd Receiving.. first second third
爲了簡單起見,咱們在最終的程序中不使用 buffered channels。但知道該使用哪一種 channel 是很重要的。
注意: 使用 buffered channels 並不會避免阻塞發生。例如,若是尋礦 gopher 比開礦 gopher 執行速度快 10 倍,而且它們經過一個容量爲 2 的 buffered channel 進行通訊,那麼尋礦 gopher 仍會發生屢次阻塞。
如今憑藉 goroutines 和 channels 的強大功能,咱們可使用 Go 的併發原語編寫一個充分發揮多線程優點的程序了。
theMine := [5]string{"rock", "ore", "ore", "rock", "ore"} oreChannel := make(chan string) minedOreChan := make(chan string) // Finder go func(mine [5]string) { for _, item := range mine { if item == "ore" { oreChannel <- item //send item on oreChannel } } }(theMine) // Ore Breaker go func() { for i := 0; i < 3; i++ { foundOre := <-oreChannel //read from oreChannel fmt.Println("From Finder:", foundOre) minedOreChan <-"minedOre" //send to minedOreChan } }() // Smelter go func() { for i := 0; i < 3; i++ { minedOre := <-minedOreChan //read from minedOreChan fmt.Println("From Miner:", minedOre) fmt.Println("From Smelter: Ore is smelted") } }() <-time.After(time.Second * 5) // Again, you can ignore this
程序輸出以下:
From Finder: ore From Finder: ore From Miner: minedOre From Smelter: Ore is smelted From Miner: minedOre From Smelter: Ore is smelted From Finder: ore From Miner: minedOre From Smelter: Ore is smelted
相比最初的例子,已經有了很大改進!如今每一個函數都獨立地運行在各自的 goroutines 中。此外,每次處理完一塊礦石,它就會被帶進挖礦流水線的下一個階段。
爲了專一於理解 goroutines 和 channel 的基本概念,上文有些重要的信息我沒有提,若是不知道的話,當你開始編程時它們可能會形成一些麻煩。既然你已經理解了 goroutines 和 channel 的工做原理,在開始用它們編寫代碼以前,讓咱們先了解一些你應該知道的其餘信息。
相似於如何利用 go 關鍵字使一個函數運行在本身的 goroutine 中,咱們能夠用以下方式建立一個匿名函數並運行在它的 goroutine 中:
// Anonymous go routine go func() { fmt.Println("I'm running in my own go routine") }()
若是隻須要調用一次函數,經過這種方式咱們可讓它在本身的 goroutine 中運行,而不須要建立一個正式的函數聲明。
main 函數確實運行在本身的 goroutine 中!更重要的是要知道,一旦 main 函數返回,它將關掉當前正在運行的其餘 goroutines。這就是爲何咱們在 main 函數的最後設置了一個定時器—它建立了一個 channel,並在 5 秒後發送一個值。
<-time.After(time.Second * 5) // Receiving from channel after 5 sec
還記得 goroutine 從 channel 中讀數據如何被阻塞直到有數據發送到裏面吧?經過添加上面這行代碼,main routine 將會發生這種狀況。它會阻塞,以給其餘 goroutines 5 秒的時間來運行。
如今有更好的方式阻塞 main 函數直到其餘全部 goroutines 都運行完。一般的作法是建立一個 done channel, main 函數在等待讀取它時被阻塞。一旦完成工做,向這個 channel 發送數據,程序就會結束了。
func main() { doneChan := make(chan string) go func() { // Do some work… doneChan <- "I'm all done!" }() <-doneChan // block until go routine signals work is done }
在前面的例子中咱們讓 miner 在 for 循環中迭代 3 次從 channel 中讀取數據。若是咱們不能確切知道將從 finder 接收多少塊礦石呢?
好吧,相似於對集合數據類型 (注: 如 slice) 進行遍歷,你也能夠遍歷一個 channel。
更新前面的 miner 函數,咱們能夠這樣寫:
// Ore Breaker go func() { for foundOre := range oreChan { fmt.Println("Miner: Received " + foundOre + " from finder") } }()
因爲 miner 須要讀取 finder 發送給它的全部數據,遍歷 channel 能確保咱們接收到已經發送的全部數據。
遍歷 channel 會阻塞,直到有新數據被髮送到 channel。在全部數據發送完以後避免 go routine 阻塞的惟一方法就是用 "close(channel)" 關掉 channel。
但你剛剛告訴咱們 channel 如何阻塞 goroutine 的各類情形?!沒錯,不過還有一個技巧,利用 Go 的 select case 語句能夠實現對 channel 的非阻塞讀。經過使用這這種語句,若是 channel 有數據,goroutine 將會從中讀取,不然就執行默認的分支。
myChan := 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 Msg Message!
非阻塞寫也是使用一樣的 select case 語句來實現,惟一不一樣的地方在於,case 語句看起來像是發送而不是接收。
select { case myChan <- "message": fmt.Println("sent the message") default: fmt.Println("no message sent") }