Go 利用chan嵌套chan 實現函數異步執行 順序返回值

遇到的問題

異步對於絕大多數的開發而言並不陌生,在go語言中異步的實現變得異常方便。只要在執行的方法前加一個go關鍵字就能夠實現異步操做。可是若是需求是,按照調用的前後順序(FIFO)來返回值咱們應該怎麼辦。你們都知道,一系列的方法調用若是使用了異步執行那麼就並不能保證返回的前後順序,返回的前後順序取決於每一個函數耗時的長短,耗時短的則會先返回。固然解決這個問題的辦法有不少,在最近看的一本書中發現了chan嵌套chan能夠很巧妙的實現這個需求。golang

沒解決以前

先看一下沒有使用嵌套chan的狀況。
代碼很簡單,方法operation1 內部sleep 1秒 方法operation2 內部sleep 2秒。5次調用都在goroutine中執行,結果能夠看到 5個方法大約耗時2秒。異步

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {

    resultCh := make(chan string)
    //開一個gotoutine 接受全部返回值並打印
    go replay(resultCh)
    //使用waitgroup 等待一下全部gorountie執行完畢,記錄時間
    wg := sync.WaitGroup{}

    startTime := time.Now()

    //operation1內部sleep 1秒
    //operation2內部sleep 2秒
    //若是是同步執行下列調用須要 8秒左右
    //目前用異步調用 理論上只須要2秒
    //但于丹的問題是 不能實現先進先出的需求
    operation2(resultCh, "aaa", &wg)
    operation2(resultCh, "bbb", &wg)
    operation1(resultCh, "ccc", &wg)
    operation1(resultCh, "ddd", &wg)
    operation2(resultCh, "eee", &wg)
    wg.Wait()
    endTime := time.Now()
    fmt.Printf("Process time %s", endTime.Sub(startTime))
}

func replay(resultCh chan string)(){
    for{
        fmt.Println(<-resultCh)
    }
}

func operation1(resultCh chan string, str string, wg *sync.WaitGroup)(){
    wg.Add(1)
    go func(str string,wg *sync.WaitGroup){
        time.Sleep(time.Second*1)
        resultCh <- "operation1:"+str
        wg.Done()
    }(str,wg)
}

func operation2(resultCh chan string, str string, wg *sync.WaitGroup)(){
    wg.Add(1)
    go func(str string,wg *sync.WaitGroup){
        time.Sleep(time.Second*2)
        resultCh <- "operation2:"+str
        wg.Done()
    }(str,wg)
}

結果:
執行結果雖然是很理想,執行5個方法只用了2秒。可是違背了需求的先進先出(FIFO)的規則。返回順序徹底是根據函數耗時長短來決定。函數

operation1:ddd 
operation1:ccc 
operation2:aaa 
operation2:eee 
operation2:bbb
Process time 2.002555639s

如何解決

建立一個嵌套chan,chan中的值也是一個chan,在執行的時候按照前後順序添加。在replay就會按照先進先出的順序讀取,利用chan阻塞等待第一個完成再執行下一個chan的值。
那麼這樣執行的時間是否會更長? 答案是並不會有太大的影響。性能

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    resultCh := make(chan chan string, 5000)
    wg := sync.WaitGroup{}
    go replay(resultCh)
    startTime := time.Now()
    operation2(resultCh, "aaa", &wg)
    operation2(resultCh, "bbb", &wg)
    operation1(resultCh, "ccc", &wg)
    operation1(resultCh, "ddd", &wg)
    operation2(resultCh, "eee", &wg)
    wg.Wait()
    endTime := time.Now()
    fmt.Printf("Process time %s", endTime.Sub(startTime))
}

func replay(resultCh chan chan string)(){
    for{
        //拿到一個chan 讀取值 這個時候拿到的是先進先出 由於全部方法是按順序加入chan的
        c := <- resultCh
        //讀取嵌套chan中的值,這個時候等待3秒 由於是operation2中執行了3秒 在這3綿中 其實其他的4個方法也已經執行完畢。以後的方法則不須要等待sleep的時間
        r := <-c
        fmt.Println(r)
    }
}

func operation1(ch chan chan string, str string, wg *sync.WaitGroup)(){
    //先建立一個chan 兵給到嵌套chan 佔據一個通道 這個通道是阻塞的
    c := make(chan string)
    ch <- c
    wg.Add(1)
    go func(str string){
        time.Sleep(time.Second*1)
        c <- "operation1:"+str
        wg.Done()
    }(str)
}

func operation2(ch chan chan string, str string, wg *sync.WaitGroup)(){
    c := make(chan string)
    ch <- c
    wg.Add(1)
    go func(str string){
        time.Sleep(time.Second*2)
        c <- "operation2:"+str
        wg.Done()
    }(str)
}

結果:
運行的結果仍是2秒,可是結果卻不一樣,徹底是按照咱們調用的順序返回的。嚴格按照先進先出的規則。這樣總體運行的時間其實取決於執行函數中耗時最長的那個函數。若是第一個函數耗時5秒 其他4個耗時1秒。那麼整個main函數耗時也就是5秒code

operation2:aaa
operation2:bbb
operation1:ccc
operation1:ddd
operation2:eee
Process time 2.002714923s

總結

其實解決此類問題的方法不止一個,好比在請求喝返回中添加標識等等。
但我認爲這個方法巧妙的運用了chan中嵌套chan和go語言 chan阻塞的特性來實現這個功能代碼簡潔。性能也兵沒有消耗太多,整體執行時間也並無加長。也是一個參考的方法,這種嵌套實現也能夠用到其餘須要的特殊需求中。開發

相關文章
相關標籤/搜索