這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
此篇介紹 go channel
什麼是 goroutine
They’re called goroutines because the existing terms — threads, coroutines, processes, and so on — convey inaccurate connotations. A goroutine has a simple model: it is a function executing in parallel with other goroutines in the same address space. It is lightweight, costing little more than the allocation of stack space. And the stacks start small, so they are cheap, and grow by allocating (and freeing) heap storage as required.
正如官方所言,goroutine 是一個輕量級的執行單元,相比線程開銷更小,完全由 Go 語言負責調度,是 Go 支援並發的核心。開啟一個 goroutine 非常簡單:
1234567891011 |
package mainimport ("fmt""time")func main() {go fmt.Println("goroutine message")time.Sleep(1) //1fmt.Println("main function message")} |
#1
的代碼是必須的,這是為了讓新開啟的 goroutine 有機會得到執行,開啟一個 goroutine 之後,後續的代碼會繼續執行,在上面的例子中後續代碼執行完畢程式就終止了,而開啟的 goroutine 可能還沒開始執行。
如果嘗試去掉 #1
處的代碼,程式也可能會正常運行,這是因為恰巧開啟的 goroutine 只是簡單的執行了一次輸出,如果 goroutine 中耗時稍長就會導致只能看到主一句 main function message
。
換句話話說,這裡的 time.sleep
提供的是一種調度機制,這也是 Go 中 channel 存在的目的:負責訊息傳遞和調度。
Channel
Channel 是 Go 中為 goroutine 提供的一種通訊機制,channel 是有類型的,而且是有方向的,可以把 channel 類比成 unix 中的 pipe。
1234 |
i := make(chan int)//int 類型s := make(chan string)//字串類型r := make(<-chan bool)//唯讀w := make(chan<- []int)//唯寫 |
Channel 最重要的作用就是傳遞訊息。
1234567891011121314 |
package mainimport ("fmt")func main() {c := make(chan int)go func() {fmt.Println("goroutine message")c <- 1 //1}()<-c //2fmt.Println("main function message")} |
例子中聲明了一個 int 類型的 channel,在 goroutine 中在代碼 #1
處向 channel 發送了資料 1
,在 main 中 #2
處等待資料的接收,如果 c 中沒有資料,代碼的執行將發生阻塞,直到 c 中資料接收完畢。這是 channel 最簡單的用法之一:同步 ,這種類型的 channel 沒有設定容量,稱之為 unbuffered channel。
unbuffered channel 和 buffered channel
Channel 可以設定容量,表示 channel 允許接收的訊息個數,預設的 channel 容量是 0 稱為 unbuffered channel ,對 unbuffered channel 執行 讀 操作 value := <-ch 會一直阻塞直到有資料可接收,執行 寫 操作 ch <- value 也會一直阻塞直到有 goroutine 對 channel 開始執行接收,正因為如此在同一個 goroutine 中使用 unbuffered channel 會造成 deadlock。
1234567891011 |
package mainimport ("fmt")func main() {c := make(chan int)c <- 1<-cfmt.Println("main function message")} |
執行報 fatal error: all goroutines are asleep - deadlock!
,讀和寫相互等待對方從而導致死結發生。
如果 channel 的容量不是 0,此類 channel 稱之為 buffered channel ,buffered channel 在訊息寫入個數 未達到容量的上限之前不會阻塞 ,一旦寫入訊息個數超過上限,下次輸入將會阻塞,直到 channel 有位置可以再寫入。
123456789101112131415161718 |
package mainimport ("fmt")func main() {c := make(chan int, 3)go func() {for i := 0; i < 4; i++ {c <- ifmt.Println("write to c ", i)}}()for i := 0; i < 4; i++ {fmt.Println("reading", <-c)}} |
上面的例子會輸出:
12345678 |
write to c 0reading 0write to c 1reading 1write to c 2reading 2write to c 3reading 3 |
根據上文對 buffered channel 的解釋,這個例子中 channel c
的容量是 3,在寫入訊息個數不超過 3 時不會阻塞,輸出應該是:
12345678 |
write to c 0write to c 1write to c 2reading 0reading 1reading 2write to c 3reading 3 |
問題在哪裡?問題其實是在 fmt.Println
,一次輸出就導致 goroutine 的執行發生了切換(相當於發生了 IO 阻塞),因而即使 c 沒有發生阻塞 goroutine 也會讓出執行,一起來驗證一下這個問題。
12345678910111213141516171819202122232425262728 |
package mainimport ("fmt""strconv")func main() {c := make(chan int, 3)s := make([]string, 8)var num int = 0go func() {for i := 0; i < 4; i++ {c <- inum++v := "inner=>" + strconv.Itoa(num)s = append(s, v)}}()for i := 0; i < 4; i++ {<-cnum++v := "outer=>" + strconv.Itoa(num)s = append(s, v)}fmt.Println(s)} |
這裡建立了一個 slice 用來儲存 c 進行寫入和讀取時的執行順序,num 是用來標識執行順序的,在沒有加入 Println 之前,最終 s 是 [inner=>1 inner=>2 inner=>3 inner=>4 outer=>5 outer=>6 outer=>7 outer=>8] ,輸出結果表明 c 達到容量上線之後才會發生阻塞。
相反有輸出語句的版本結果則不同:
1234567891011121314151617181920212223242526272829 |
package mainimport ("fmt""strconv")func main() {c := make(chan int, 3)s := make([]string, 8)var num int = 0go func() {for i := 0; i < 4; i++ {c <- inum++v := "inner=>" + strconv.Itoa(num)s = append(s, v)fmt.Println("write to c ", i)}}()for i := 0; i < 4; i++ {num++v := "outer=>" + strconv.Itoa(num)s = append(s, v)fmt.Println("reading", <-c)}fmt.Println(s)} |
[outer=>1 inner=>2 outer=>3 inner=>4 inner=>5 inner=>6 outer=>7 outer=>8] 輸出結果能表明兩個 goroutine 是交替執行,也就是說 IO 的調用 Println 導致 goroutine 的讓出了執行。
讀取多個 channel 的訊息
Go 提供了 select 語句來處理多個 channel 的訊息讀取。
123456789101112131415161718192021222324252627282930313233343536373839 |
package mainimport ("fmt""time")func main() {c1 := make(chan string)c2 := make(chan string)go func() {for {c1 <- "from 1"time.Sleep(time.Second * 2)}}()go func() {for {c2 <- "from 2"time.Sleep(time.Second * 2)}}()go func() {for {select {case msg1 := <-c1:fmt.Println(msg1)case msg2 := <-c2:fmt.Println(msg2)}}}()var input stringfmt.Scanln(&input)} |
select 語句可以從多個可讀的 channel 中隨機選取一個執行,注意是 隨機選取。
Channel 關閉之後
Channel 可以被關閉 close
,channel 關閉之後仍然可以讀取,如果 channel 關閉之前有值寫入,關閉之後將依次讀取 channel 中的訊息,讀完完畢之後再次讀取將會返回 channel 的類型的 zero value:
123456789101112131415161718192021 |
package mainimport ("fmt")func main() {c := make(chan int, 3)go func() {c <- 1c <- 2c <- 3close(c)}()fmt.Println(<-c)fmt.Println(<-c)fmt.Println(<-c)fmt.Println(<-c)fmt.Println(<-c)fmt.Println(<-c)} |
輸出 1 2 3 0 0 0 ,0 是 int channel c 的 zero value。
被關閉的 channel 可以進行 range 迭代:
123456789101112131415161718 |
package mainimport ("fmt")func main() {c := make(chan int, 3)go func() {c <- 1c <- 2c <- 3close(c)}()for i := range c {fmt.Println(i)}} |
未被關閉的 channel 則不行,如果沒有被關閉,range 在輸出完 channel 中的訊息之後將會阻塞一直等待,從而發生死結。
參考資料
- https://www.miek.nl/go
- http://guzalexander.com/2013/12/06/golang-channels-tutorial.html