Channel使用技巧
- 2019 年 10 月 3 日
- 筆記
前言
Go協程一般使用channel(通道)通訊從而協調/同步他們的工作。合理利用Go協程和channel能幫助我們大大提高程式的性能。本文將介紹一些使用channel的場景及技巧
場景一,使用channel返回運算結果
計算斐波那契數列,在學習遞歸時候這是個經典問題。現在我們不用遞歸實現,而是用channel返回計算得出的斐波那契數列。 計算前40個斐波那契數列的值,看下效率
package main import ( "fmt" "time" ) //計算斐波那契數列並寫到ch中 func fibonacci(n int, ch chan<- int) { first, second := 1, 1 for i := 0; i < n; i++ { ch <- first first, second = second, first+second } close(ch) } func main() { ch := make(chan int, 40) i := 0 start := time.Now() go fibonacci(cap(ch), ch) for result := range ch { fmt.Printf("fibonacci(%d) is: %dn", i, result) i++ } end := time.Now() delta := end.Sub(start) fmt.Printf("took the time: %sn", delta) }
只花了7ms,效率是遞歸實現的100倍(主要是演算法效率問題)
fibonacci(33) is: 5702887 fibonacci(34) is: 9227465 fibonacci(35) is: 14930352 fibonacci(36) is: 24157817 fibonacci(37) is: 39088169 fibonacci(38) is: 63245986 fibonacci(39) is: 102334155 took the time: 8.0004ms
使用for-range讀取channel返回的結果十分便利。當channel關閉且沒有數據時,for循環會自動退出,無需主動監測channel是否關閉。close(ch)只針對寫數據到channel起作用,意思是close(ch)後,ch中不能再寫數據,但不影響從ch中讀數據
場景二,使用channel獲取多個並行方法中的一個結果
假設程式從多個複製的資料庫同時讀取。只需要接收首先到達的一個答案,Query 函數獲取資料庫的連接切片並請求。並行請求每一個資料庫並返回收到的第一個響應:
func Query(conns []conn, query string) Result { ch := make(chan Result, 1) for _, conn := range conns { go func(c Conn) { select { case ch <- c.DoQuery(query): } }(conn) } return <- ch }
場景三,響應超時處理
在調用遠程方法的時候,存在超時可能,超時後返回超時提示
func CallWithTimeOut(timeout time.Duration) (int, error) { select { case resp := <-Call(): return resp, nil case <-time.After(timeout): return -1, errors.New("timeout") } } func Call() <-chan int { outCh := make(chan int) go func() { //調用遠程方法 }() return outCh }
同樣可以擴展到channel的讀寫操作
func ReadWithTimeOut(ch <-chan int) (x int, err error) { select { case x = <-ch: return x, nil case <-time.After(time.Second): return 0, errors.New("read time out") } } func WriteWithTimeOut(ch chan<- int, x int) (err error) { select { case ch <- x: return nil case <-time.After(time.Second): return errors.New("read time out") } }
使用<-time.After()超時設置可能引發的記憶體泄露問題,可以看這篇文章
場景四,多任務並發執行和順序執行
方法A和B同時執行,方法C等待方法A執行完後才能執行,main等待A、B、C執行完才退出
package main import ( "fmt" "time" ) func B(quit chan<- string) { fmt.Println("B crraied out") quit <- "B" } func A(quit chan<- string, finished chan<- bool) { // 模擬耗時任務 time.Sleep(time.Second * 1) fmt.Println("A crraied out") finished <- true quit <- "A" } func C(quit chan<- string, finished <-chan bool) { // 在A沒有執行完之前,finished獲取不到數據,會阻塞 <-finished fmt.Println("C crraied out") quit <- "C" } func main() { finished := make(chan bool) defer close(finished) quit := make(chan string) defer close(quit) go A(quit, finished) go B(quit) go C(quit, finished) fmt.Println(<-quit) fmt.Println(<-quit) fmt.Println(<-quit) }
正常執行我們得到以下結果
B crraied out B A crraied out A C crraied out C
注意:最後從quit中讀數據不能使用for-range語法,不然程式會出現死鎖
for res := range quit { fmt.Println(res) }
fatal error: all goroutines are asleep - deadlock!
原因很簡單,程式中quit通道沒有被close,A、B、C運行完了,Go的主協程在for循環中阻塞了,所有Go協程都阻塞了,進入了死鎖狀態
場景五,超時後停止Go協程,避免浪費資源(停止調用鏈)
場景四中,假設A方法掛了或者需要執行很長時間,main協程會等到所有方法執行完才會退出。在實際應用中顯然不行,所以要設置超時時間。問題來了,C方法是基於A方法執行完後才執行的,我們怎樣通知C方法退出呢。這裡針對普通的Go協程,不是Http請求,有關Http超時問題引起的記憶體泄露可以看這篇文章
下面我們修改場景四的程式碼,讓A方法有超時設置,C方法在A方法超時後也退出
package main import ( "fmt" "time" ) // B方法 func B(quit chan<- string) { fmt.Println("B crraied out") quit <- "B" } // A方法,有超時限制 func AWithTimeOut(quit chan<- string, finishedA chan<- bool, timeout time.Duration) { select { case resp := <-A(finishedA): quit <- resp case <-time.After(timeout): quit <- "A timeout" } } // A需要執行的任務 func A(finishedA chan<- bool) <-chan string { respCh := make(chan string) go func() { // 模擬耗時任務 // time.Sleep(time.Second * 3) fmt.Println("A crraied out") finishedA <- true respCh <- "A" }() return respCh } // C方法,等待A方法完成後才能執行,同樣有超時限制,超時時間和A方法一致 func CWithTimeOut(quit chan<- string, finishedA <-chan bool, timeout time.Duration) { select { case <-finishedA: fmt.Println("C crraied out") quit <- "C" case <-time.After(timeout): fmt.Println("C Exited") quit <- "C timeout" } } func main() { finishedA := make(chan bool, 1) //這裡必須要是1的緩衝通道,不然超時後會死鎖 defer close(finishedA) quit := make(chan string, 3) defer close(quit) timeout := time.Second * 2 go AWithTimeOut(quit, finishedA, timeout) go B(quit) go CWithTimeOut(quit, finishedA, timeout) fmt.Println(<-quit) fmt.Println(<-quit) fmt.Println(<-quit) time.Sleep(time.Second * 3) //如果程式未退出的話,A方法執行的任務還會繼續運行,因為我們沒辦法讓A方法停下來 }
運行結果
B crraied out B C Exited C timeout A timeout A crraied out
A方法用
time.Sleep(time.Second * 3)
模擬超時任務,程式碼最後讓main協程休眠,主要為了說明雖然A超時了,但正常情況下它還是會把任務執行下去的。如果有哪位大俠有什麼方法能讓它不執行,還請告知!!!
總結
本文介紹了幾種場景下channel的使用技巧,希望能起到拋磚引玉的作用,各位如有其它技巧,歡迎評論,本文會把你們的技巧收納在其中。感謝!!!