天天看點

Channel使用技巧

前言

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: %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
}
           

場景三,響應逾時處理

在調用遠端方法的時候,存在逾時可能,逾時後傳回逾時提示

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()逾時設定可能引發的記憶體洩露問題,可以看這篇文章

逾時後使用context取消goroutine執行的方法,參考:https://mp.weixin.qq.com/s/780-KicWIQZNwmAtTAHmaw

場景四,多任務并發執行和順序執行

方法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協程都阻塞了,進入了死鎖狀态

總結

本文介紹了幾種場景下channel的使用技巧,希望能起到抛磚引玉的作用,各位如有其它技巧,歡迎評論,本文會把你們的技巧收納在其中。感謝!!!

看完之後若覺得對自己有幫助,懇請點贊或評論。這是對我最大的鼓勵!