
前言
學習Golang 讓人最興奮的大概就是它內建的 Concurrency 的支援,並且相當的容易使用. 但是最近在學習與使用的時候,發現發現 Goroutine 可以讓本來需要用到 40 秒的 Request 減少到1/3 左右的時間.進而可以進步到 5 秒左右.平常的應用上,幾乎已經離不開 Goroutine 與Channel。
但是最近遇到幾個例子,一開始覺得很不能理解,也不容易解決.也是讓我找了一些解決方式.
一般用法
Go Channel一般有兩種用法(當然有更多的使用方式可以用),一個是把channel當成資料來傳遞,另外的方式只是單純把channel當成是signal來等待或是啟動新的thread
// 同時要處理多個可以同步運行的運算,利用channel來回傳結果
c := make(chan int) // Allocate a channel.
go func() {
result : = do_some_cal()
c <- result
}()
c2 := make(chan int) // Allocate a channel.
go func() {
result : = do_some_cal2()
c2 <- result
}()
totalResult := <-c2 + <-c
fmt.Printf("result is %d"), totalResult)
// 傳送訊號(signal)的方式
c := make(chan int) // Allocate a channel.
go func() {
list.Sort()
c <- 1 // Send a signal; value does not matter.
}()
doSomethingForAWhile()
<-c // Wait for sort to finish; discard sent value.
陷阱出現了
以上兩個方式相當的簡單,也相當直覺來使用.但是如果要跑goroutine的條件不是必要呢?也就是說不一定要透過go routine來同步執行很多的結果呢?
c := make(chan int)
//並不一定會進入go routine來計算,也就是說channel會是為空的.
if needRoutine == true {
go func() {
result : = do_some_cal()
c <- result
}()
} else {
c <- 0 //note this kind will not work, because channels need input value in go routine.
}
// 這裏將會變成deadlock,而無窮的等待.
totalResult := <-c * 3 + 5
fmt.Printf("result is %d"), totalResult)
上面的例子可以看到,當needRoutine為false的時候,由於不會去跑go routine,所以 <-c會變成是deadlock.
此外,如果你試著在else 的地方自己加上 c <- 0,也是沒有任何作用.
所以尋找過後,有一些解決的方式可以參考.
解決方式
由邏輯來解決
比較簡單方式,並且code上面比較不會那麼複雜的方式是.就是在Go routine裡面去做額外的處理.也就是都會進去Go routine而在Go routine中再做檢查而回傳.
c := make(chan int)
go func() {
//在Go Routine中處理
if needRoutine == false {
c <- 0 //or other invalid value
return
}
result : = do_some_cal()
c <- result
}()
// 這樣就不會變成無窮等待
result := <-c
if result > 0 {
totalResult := result * 3 + 5
fmt.Printf("result is %d"), totalResult)
}
透過selector
透過Selector其實可以處理的事情相當的多. 它不僅僅可以分派處理很多個需求,先講解一些方式.
透過timer
透過時間檢查, 過了特定時間就放棄不等. 這種解法比較好,只要在特定時間內如果能收到數值,即時還是可以順利得到結果.
package main
import "time"
import "fmt"
func main() {
c1 := make(chan string, 1)
go func() {
time.Sleep(time.Second * 2)
c1 <- "result 1"
}()
select {
case res := <-c1:
fmt.Println(res)
case <-time.After(time.Second * 1):
fmt.Println("timeout 1")
}
}
透過檢查channel有沒有設定過
以下例子可以檢查,如果channel有沒有設定過,才決定要不要拿來用. 不過這種狀況下,如果你的Goroutine需要一點時間無法馬上傳回解答的時候,這個時候就會馬上離開.
所以,如果原本到這個時候就應該有解答(不需要任何等待(non block)),可以使用這種方式.不然的話還是建議使用timer的方式.
package main
import "time"
import "fmt"
func main() {
c1 := make(chan string, 1)
if someCondition == true {
go func() {
time.Sleep(time.Second * 2)
c1 <- "result 1"
}()
}
select {
case x, ok := <-c1: //如果someCondition == true 除非這時候剛好得到結果,不然跑不到.
if ok {
fmt.Printf("Value %d was read.\n", x)
} else {
fmt.Println("Channel closed!") //Channel 被close.
}
default:
fmt.Println("No value ready, moving on.") //Channel 沒有設定過,會馬上離開....
}
}
Refer here.
進階的用法
這裏附上另外一個select的用法,就是來取得多個channel的資料.並且也用一個timer來顯示時間經過了幾秒. 全部做完才會離開…. 不然就是超過一定時間也會離開,避免有dealock產生.
package main
import "time"
import "fmt"
func main() {
c1 := make(chan string, 1)
someCondition := true
if someCondition == true {
go func() {
time.Sleep(time.Second * 4)
c1 <- "result 1"
}()
}
c2 := make(chan string, 1)
if someCondition == true {
go func() {
//故意延遲九秒,所以這個不會順利結束.
time.Sleep(time.Second * 9)
c2 <- "result 1"
}()
}
doneCount := 0
allDone := 2
timeCount := 0
// 別忘了... select 滿足任何一個都會離開,所以要有個for在外面讓他不停跑
for doneCount < allDone && timeCount < 5 {
select {
//檢查C1
case x, ok := <-c1:
if ok {
fmt.Printf("C1 in valus is %s.\n", x)
doneCount++
} else {
fmt.Println("Channel closed!") //Channel 被close.
}
//檢查C2
case x, _ := <-c2:
fmt.Printf("C2 in valus is %s.\n", x)
doneCount++
//另外準備一個離開條件,當五秒會離開...
case <-time.After(time.Second * 1):
fmt.Println("tick..")
timeCount++
}
}
}
回過頭來看.. channel buffer..
關於channel沒經過goroutine會出問題的原因,經過尋找過後總算找到原因了. 可以參考以下這篇文章.
By default channels are unbuffered, meaning that they will only accept sends (chan <-) if there is a corresponding receive (<- chan) ready to receive the sent value. (from [Go Example](https://gobyexample.com/channel-buffering))
也就是說,如果你沒有幫channel 設定好buffer (也就是 make(chan int, 1))先給他個預設空間的話. channel <- value 會一直等到…. <- channel 才會順利結束. 這也是如果你直接使用channel 而沒有進入goroutine或是給予buffer就會deadlock
package main
import "fmt"
func main() {
messages := make(chan string)
messages <- "buffered" //會有deadlock,因為會停到直到跑到 <-messages
messages <- "channel" //會有deadlock,因為會停到直到跑到 <-messages
fmt.Println(<-messages)
fmt.Println(<-messages)
}
正確的解法,就是要給予channel buffer
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int, 1) //Add buffer cause not deadlock. http://stackoverflow.com/questions/14050673/why-are-my-channels-deadlocking
needRoutine := false
if needRoutine {
go func() {
time.Sleep(time.Second * 3)
c <- 3
}()
} else {
c <- 0
}
totalResult := <-c*3 + 5
fmt.Printf("result is %d", totalResult)
}
Go play is here
參考資料
- Effective Go: Channels
- 官方Effective Go文件,一定要熟讀.
- Concurrency, Goroutines and GOMAXPROCS
- 算是簡介文章與範例說明….
- Go by Example: Timeouts
- Go by example 是最好學習Go的方式,並且搭配著Go Play可以馬上看結果.
- Stackoverflow : Checking if a channel has a ready-to-read value, using Go
- Slideshare: Go for the would be network programmer
- Slideshare: RubyConf Portugal 2014 - Why ruby must go!