Channel使用技巧

前言

Go協程通常使用channel(通道)通訊從而協調/同步他們的工做。合理利用Go協程和channel能幫助咱們大大提升程序的性能。本文將介紹一些使用channel的場景及技巧golang

場景一,使用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: %d\n", i, result)
        i++
    }
    end := time.Now()
    delta := end.Sub(start)
    fmt.Printf("took the time: %s\n", 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
}

場景三,響應超時處理

在調用遠程方法的時候,存在超時可能,超時後返回超時提示post

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執行完才退出ui

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)
}

正常執行咱們獲得如下結果code

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的使用技巧,但願能起到拋磚引玉的做用,各位若有其它技巧,歡迎評論,本文會把大家的技巧收納在其中。感謝!!!

相關文章
相關標籤/搜索