goroutine,channel

Go語言中有個概念叫作goroutine, 這相似咱們熟知的線程,可是更輕。
如下的程序,咱們串行地去執行兩次loop函數:python

package main

import "fmt"

func main() {
    loop()
    loop()
}

func loop() {
    for i := 0; i < 10; i++ {
        fmt.Printf("%d ", i)
    }
}

毫無疑問,輸出會是這樣的:shell

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

下面咱們把一個loop放在一個goroutine裏跑,咱們可使用關鍵字go來定義並啓動一個goroutine,屢次運行:編程

0 1 2 3 4 5 6 7 8 9 
//或有多是下面這樣
0 1 2 3 4 5 6 7 8 9 0 1 2 
//亦或是下面這樣
0 1 2 3 4 5 6 7 8 9 0

咱們反覆運行上面的代碼會發現結果會相似於上面這樣,可是就是沒法完整輸出兩遍0~9,明明咱們主線跑了一趟,也開了一個goroutine來跑一趟啊。緩存

原來,在goroutine還沒來得及跑loop的時候,主函數已經退出了,正所謂"皮(主線程)之不存,毛(子線程)將焉附"。安全

main函數退出地太快了,咱們要想辦法阻止它過早地退出,一個辦法是讓main等待一下:併發

package main

import (
    "fmt"
    "time"
)

func main() {
    go loop() //啓動一個goroutine
    loop()
    time.Sleep(time.Second) //停頓一秒
}

func loop() {
    for i := 0; i < 10; i++ {
        fmt.Printf("%d ", i)
    }
}

此次確實輸出了兩趟,目的達到了。
但是採用等待的辦法並很差,若是goroutine在結束的時候,告訴下主線說「Hey, 我要跑完了!」就行了, 即所謂阻塞主線的辦法,回憶下咱們Python裏面等待全部線程執行完畢的寫法:函數

for thread in threads:
    thread.join()

是的,咱們也須要一個相似join的東西來阻塞住主線。那就是信道oop

信道

信道是什麼?簡單說,是goroutine之間互相通信的東西。相似咱們Unix上的管道(能夠在進程間傳遞消息), 用來goroutine之間發消息和接收消息。其實,就是在作goroutine之間的內存共享。
使用make來創建一個信道:測試

var channel chan int = make(chan int)

那如何向信道存消息和取消息呢? 一個例子:ui

package main

import "fmt"

func main() {
    var messages = make(chan string)
    go func(message string) {
        messages <- message //存消息
    }("Ping!")

    fmt.Println(<-messages) //取消息
}

默認的,信道的存消息和取消息都是阻塞的 (叫作無緩衝的信道,不過緩衝這個概念稍後瞭解,先說阻塞的問題)。

也就是說, 無緩衝的信道在取消息和存消息的時候都會掛起當前的goroutine,除非另外一端已經準備好。
好比如下的main函數和foo函數:

package main

var ch = make(chan int)

func foo() {
    ch <- 0 //向ch中加數據,若是沒有其餘goroutine來取走這個數據,那麼掛起foo, 直到main函數把0這個數據拿走
}
func main() {
    go foo()
    <-ch //從ch取數據,若是ch中還沒放數據,那就掛起main線,直到foo函數中放數據爲止
}

那既然信道能夠阻塞當前的goroutine, 那麼回到上一部分「goroutine」所遇到的問題「如何讓goroutine告訴主線我執行完畢了」 的問題來, 使用一個信道來告訴主線便可:

package main

import "fmt"

var ch = make(chan int)

func loop() {
    for i := 0; i < 10; i++ {
        fmt.Printf("%d ", i)
    }
    ch <- 0 //執行完畢了,發個消息
}

func main() {

    go loop()
    <- ch //main在此阻塞住,直到線程跑完, 取到消息.
}

若是不用信道來阻塞主線的話,主線就會過早跑完,loop線都沒有機會執行

其實,無緩衝的信道永遠不會存儲數據,只負責數據的流通,爲何這麼講呢?

  • 從無緩衝信道取數據,必需要有數據流進來才能夠,不然當前線阻塞
  • 數據流入無緩衝信道, 若是沒有其餘goroutine來拿走這個數據,那麼當前線阻塞

因此,你能夠測試下,不管如何,咱們測試到的無緩衝信道的大小都是0 (len(channel))
若是信道正有數據在流動,咱們還要加入數據,或者信道乾澀,咱們一直向無數據流入的空信道取數據呢? 就會引發死鎖
死鎖一個死鎖的例子:

package main

func main() {
    ch := make(chan int)

    <-ch //阻塞main goroutine,信道ch被鎖
}

執行這個程序你會看到Go報這樣的錯誤:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
    /Users/XXX/Go/src/main.go:6 +0x4d

Process finished with exit code 2

何謂死鎖? 操做系統有講過的,全部的線程或進程都在等待資源的釋放。如上的程序中, 只有一個goroutine, 因此當你向裏面加數據或者存數據的話,都會鎖死信道, 而且阻塞當前 goroutine, 也就是全部的goroutine(其實就main線一個)都在等待信道的開放(沒人拿走數據信道是不會開放的),也就是死鎖咯。
我發現死鎖是一個頗有意思的話題,這裏有幾個死鎖的例子:
1.只在單一的goroutine裏操做無緩衝信道,必定死鎖。好比你只在main函數裏操做信道:

package main

import "fmt"

func main() {
    ch := make(chan int)

    ch <- 1 //1流入信道,堵塞當前線, 沒人取走數據信道不會打開
    fmt.Println("This line code won`t run") //在此行執行以前Go就會報死鎖
}

2.以下也是一個死鎖的例子:

package main

import "fmt"

var ch1 = make(chan int)
var ch2 = make(chan int)

func say(s string) {
    fmt.Println(s)
    ch1 <- <- ch2 //ch1等待ch2流出的數據
}

func main() {
    go say("hello")
    <-ch1 //堵塞主線
}

其中主線等ch1中的數據流出,ch1等ch2的數據流出,可是ch2等待數據流入,兩個goroutine都在等,也就是死鎖。

3.其實,總結來看,爲何會死鎖?非緩衝信道上若是發生了流入無流出,或者流出無流入,也就致使了死鎖。或者這樣理解 Go啓動的全部goroutine裏的非緩衝信道必定要一個線裏存數據,一個線裏取數據,要成對才行 。因此下面的示例必定死鎖:

package main

func main() {
    c, quit := make(chan int), make(chan int)

    go func() {
        c <- 1 //c通道的數據沒有被其餘goroutine讀取走,堵塞當前goroutine
        quit <- 0 //quit始終沒有辦法寫入數據
    }()
    
    <- quit //quit等待數據的寫
}

仔細分析的話,是因爲:主線等待quit信道的數據流出,quit等待數據寫入,而func被c通道堵塞,全部goroutine都在等,因此死鎖。
簡單來看的話,一共兩個線,func線中流入c通道的數據並無在main線中流出,確定死鎖。

可是,是否果然 全部不成對向信道存取數據的狀況都是死鎖?

以下是個反例:

package main

func main() {

    c := make(chan int)

    go func() {
        c <- 1
    }()
}

程序正常退出了,很簡單,並非咱們那個總結不起做用了,仍是由於一個讓人很囧的緣由,main又沒等待其它goroutine,本身先跑完了, 因此沒有數據流入c信道,一共執行了一個goroutine, 而且沒有發生阻塞,因此沒有死鎖錯誤。

那麼死鎖的解決辦法呢?
最簡單的,把沒取走的數據取走,沒放入的數據放入, 由於無緩衝信道不能承載數據,那麼就趕忙拿走!
具體來說,就死鎖例子3中的狀況,能夠這麼避免死鎖:

package main

func main() {
    c, quit := make(chan int), make(chan int)

    go func() {
        c <- 1 //c通道的數據沒有被其餘goroutine讀取走,堵塞當前goroutine
        quit <- 0 //quit始終沒有辦法寫入數據
    }()
    
    <- c    //取走c的數據
    <- quit //quit等待數據的寫
}

另外一個解決辦法是緩衝信道, 即設置c有一個數據的緩衝大小:

c := make(chan int, 1)

這樣的話,c能夠緩存一個數據。也就是說,放入一個數據,c並不會掛起當前線, 再放一個纔會掛起當前線直到第一個數據被其餘goroutine取走, 也就是隻阻塞在容量必定的時候,不達容量不阻塞。

無緩衝信道的數據進出順序咱們已經知道,無緩衝信道從不存儲數據,流入的數據必需要流出才能夠。
觀察如下的程序:

package main

import "fmt"

var ch chan int = make(chan int)

func foo(id int) {
    ch <- id
}
func main() {

    //開啓5個routine
    for i := 0; i < 5; i++ {
        go foo(i)
    }

    //取出信道中的數據
    for i := 0; i < 5; i++ {
        fmt.Print(<- ch)
    }
}

們開了5個goroutine,而後又依次取數據。其實整個的執行過程細分的話,5個線的數據 依次流過信道ch, main打印之, 而宏觀上咱們看到的即 無緩衝信道的數據是先到先出,可是 無緩衝信道並不存儲數據,只負責數據的流通

緩衝信道

終於到了這個話題了, 其實緩存信道用英文來說更爲達意: buffered channel.

緩衝這個詞意思是,緩衝信道不只能夠流通數據,還能夠緩存數據。它是有容量的,存入一個數據的話 , 能夠先放在信道里,沒必要阻塞當前線而等待該數據取走。

當緩衝信道達到滿的狀態的時候,就會表現出阻塞了,由於這時不再能承載更多的數據了,「大家必須把 數據拿走,才能夠流入數據」

在聲明一個信道的時候,咱們給make以第二個參數來指明它的容量(默認爲0,即無緩衝):

var ch chan int = make(chan int, 2) // 寫入2個元素都不會阻塞當前goroutine, 存儲個數達到2的時候會阻塞

以下的例子,緩衝信道ch能夠無緩衝的流入3個元素:

package main

func main() {
    ch := make(chan int, 3)
    ch <- 1
    ch <- 2
    ch <- 3   
}

若是你再試圖流入一個數據的話,信道ch會阻塞main線, 報死鎖。
也就是說,緩衝信道會在滿容量的時候加鎖。

其實,緩衝信道是先進先出的,咱們能夠把緩衝信道看做爲一個線程安全的隊列:

package main

import "fmt"

func main() {

    ch := make(chan int, 3)
    ch <- 1
    ch <- 2
    ch <- 3

    fmt.Println(<-ch) //1
    fmt.Println(<-ch) //2
    fmt.Println(<-ch) //3
}

信道數據讀取和信道關閉你也許發現,上面的代碼一個一個地去讀取信道簡直太費事了,Go語言容許咱們使用range來讀取信道:

package main

import "fmt"

func main() {

    ch := make(chan int, 3)
    ch <- 1
    ch <- 2
    ch <- 3

    for v := range ch {
        fmt.Println(v)
    }
}

若是你執行了上面的代碼,會報死鎖錯誤的,緣由是range不等到信道關閉是不會結束讀取的。也就是若是 緩衝信道乾涸了,那麼range就會阻塞當前goroutine, 因此死鎖咯。

那麼,咱們試着避免這種狀況,比較容易想到的是讀到信道爲空的時候就結束讀取:

package main

import "fmt"

func main() {

    ch := make(chan int, 3)
    ch <- 1
    ch <- 2
    ch <- 3

    for v := range ch {
        fmt.Println(v)
        if len(ch) <= 0 { //若是如今數據量爲0,跳出循環
            break
        }
    }
}

以上的方法是能夠正常輸出的,可是注意檢查信道大小的方法不能在信道存取都在發生的時候用於取出全部數據,這個例子 是由於咱們只在ch中存了數據,如今一個一個往外取,信道大小是遞減的。

另外一個方式是顯式地關閉信道:

package main

import "fmt"

func main() {

    ch := make(chan int, 3)
    ch <- 1
    ch <- 2
    ch <- 3

    //顯式地關閉信道
    close(ch)

    for v := range ch {
        fmt.Println(v)
    }
}

被關閉的信道會禁止數據流入, 是隻讀的。咱們仍然能夠從關閉的信道中取出數據,可是不能再寫入數據了。

等待多gorountine的方案那好,咱們回到最初的一個問題,使用信道堵塞主線,等待開出去的全部goroutine跑完。

這是一個模型,開出不少小goroutine, 它們各自跑各自的,最後跑完了向主線報告。

咱們討論以下2版本的方案:

  1. 只使用單個無緩衝信道阻塞主線
  2. 使用容量爲goroutines數量的緩衝信道

對於方案1, 示例的代碼大概會是這個樣子:

package main

import "fmt"

var quit chan int //只開一個信道

func foo(id int) {
    fmt.Println(id)
    quit <- 0 //ok,finished
}

func main() {

    count := 1000
    quit = make(chan int) //無緩衝

    for i := 0; i < count; i++ {
        go foo(i)
    }

    for i := 0; i < count; i++ {
        <- quit
    }
}

對於方案2,把信道換成1000的:

quit = make(chan int, count) // 容量1000

其實區別僅僅在於一個是緩衝的,一個是非緩衝的。
對於這個場景而言,二者都能完成任務, 都是能夠的。

  • 無緩衝的信道是一批數據一個一個的「流進流出」
  • 緩衝信道則是一個一個存儲,而後一塊兒流出去

轉載:
Go編程基礎—併發

相關文章
相關標籤/搜索