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的使用技巧,希望能起到拋磚引玉的作用,各位如有其它技巧,歡迎評論,本文會把你們的技巧收納在其中。感謝!!!