圖解 Go 併發

你極可能從某種途徑據說過 Go 語言。它愈來愈受歡迎,而且有充分的理由能夠證實。 Go 快速、簡單,有強大的社區支持。學習這門語言最使人興奮的一點是它的併發模型。 Go 的併發原語使建立多線程併發程序變得簡單而有趣。我將經過插圖介紹 Go 的併發原語,但願能點透相關概念以方便後續學習。本文是寫給 Go 語言編程新手以及準備開始學習 Go 併發原語 (goroutines 和 channels) 的同窗。編程

單線程程序 vs. 多線程程序

你可能已經寫過一些單線程程序。一個經常使用的編程模式是組合多個函數來執行一個特定任務,而且只有前一個函數準備好數據,後面的纔會被調用。多線程

single Gopher

首先咱們將用上述模式編寫第一個例子的代碼,一個描述挖礦的程序。它包含三個函數,分別負責執行尋礦、挖礦和練礦任務。在本例中,咱們用一組字符串表示 rock(礦山) 和 ore(礦石),每一個函數都以它們做爲輸入,並返回一組 「處理過的」 字符串。對於一個單線程的應用而言,該程序可能會按以下方式來設計:併發

ore mining single-threaded program

它有三個主要的函數:finderminer 和 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

ore mining concurrent program

這種設計使得 「挖礦」 更高效。如今多個線程 (gophers) 是獨立運行的,從而 Gary 再也不承擔所有工做。其中一個 gopher 負責尋礦,一個負責挖礦,另外一個負責練礦,這些工做可能同時進行。this

爲了將這種併發特性引入咱們的代碼,咱們須要建立獨立運行的 gophers 的方法以及它們之間彼此通訊 (傳送礦石) 的方法。這就須要用到 Go 的併發原語:goroutines 和 channels。spa

Goroutines

Goroutines 能夠看做是輕量級線程。建立一個 goroutine 很是簡單,只須要把 go 關鍵字放在函數調用語句前。爲了說明這有多麼簡單,咱們建立兩個 finder 函數,並用 go 調用,讓它們每次找到 "ore" 就打印出來。線程

go myFunc()

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

communication

Channels 容許 go routines 之間相互通訊。你能夠把 channel 看做管道,goroutines 能夠往裏面發消息,也能夠從中接收其它 go routines 的消息。

my first channel

myFirstChannel := make(chan string) 

Goroutines 能夠往 channel 發送消息,也能夠從中接收消息。這是經過箭頭操做符 (<-) 完成的,它指示 channel 中的數據流向。

arrow

myFirstChannel <-"hello" // Send myVariable := <- myFirstChannel // Receive 

如今經過 channel 咱們可讓尋礦 gopher 一找到礦石就當即傳送給開礦 gopher ,而不用等發現全部礦石。

ore channel

我重寫了挖礦程序,把尋礦和開礦函數改寫成了未命名函數。若是你從未見過 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 寫複雜的程序以前,咱們先來理解它的一些關鍵特性。

Channel Blocking

Channels 阻塞 goroutines 發生在各類情形下。這能在 goroutines 各自歡快地運行以前,實現彼此之間的短暫同步。

Blocking on a Send

blocking on send

一旦一個 goroutine(gopher) 向一個 channel 發送數據,它就被阻塞了,直到另外一個 goroutine 從該 channel 取走數據。

Blocking on a Receive

blocking on receive

和發送時情形相似,一個 goroutine 可能阻塞着等待從一個 channel 獲取數據,若是尚未其餘 goroutine 往該 channel 發送數據。

一開始接觸阻塞的概念可能使人有些困惑,但你能夠把它想象成兩個 goroutines(gophers) 之間的交易。 其中一個 gopher 不管是等着收錢仍是送錢,都須要等待交易的另外一方出現。

既然已經瞭解 goroutine 經過 channel 通訊可能發生阻塞的不一樣情形,讓咱們討論兩種不一樣類型的 channels: unbuffered 和 buffered 。選擇使用哪種 channel 可能會改變程序的運行表現。

Unbuffered Channels

unbuffered channel

在前面的例子中咱們一直在用 unbuffered channels,它們不同凡響的地方在於每次只有一份數據能夠經過。

Buffered Channels

buffered channel

在併發程序中,時間協調並不老是完美的。在挖礦的例子中,咱們可能遇到這樣的情形:開礦 gopher 處理一塊礦石所花的時間,尋礦 gohper 可能已經找到 3 塊礦石了。爲了避免讓尋礦 gopher 浪費大量時間等着給開礦 gopher 傳送礦石,咱們可使用 buffered channel。咱們先建立一個容量爲 3 的 buffered channel。

bufferedChan := make(chan string, 3) 

buffered 和 unbuffered channels 工做原理相似,但有一點不一樣—在須要另外一個 gorountine 取走數據以前,咱們能夠向 buffered channel 發送多份數據。

cap 3 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 的併發原語編寫一個充分發揮多線程優點的程序了。

putting it all together

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 的工做原理,在開始用它們編寫代碼以前,讓咱們先了解一些你應該知道的其餘信息。

在開始以前,你應該知道...

匿名的 Goroutines

anonymous goroutine

相似於如何利用 go 關鍵字使一個函數運行在本身的 goroutine 中,咱們能夠用以下方式建立一個匿名函數並運行在它的 goroutine 中:

// Anonymous go routine
go func() {
    fmt.Println("I'm running in my own go routine")
}()

  

若是隻須要調用一次函數,經過這種方式咱們可讓它在本身的 goroutine 中運行,而不須要建立一個正式的函數聲明。

main 函數是一個 goroutine

main func

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 發送數據,程序就會結束了。

done chan

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
}

  

你能夠遍歷 channel

在前面的例子中咱們讓 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 進行非阻塞讀

但你剛剛告訴咱們 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!

對 channel 進行非阻塞寫

非阻塞寫也是使用一樣的 select case 語句來實現,惟一不一樣的地方在於,case 語句看起來像是發送而不是接收。

select { case myChan <- "message": fmt.Println("sent the message") default: fmt.Println("no message sent") } 
相關文章
相關標籤/搜索