go學習筆記 利用chan巢狀chan 實現函式非同步執行 順序返回值
遇到的問題
非同步對於絕大多數的開發而言並不陌生,在go語言中非同步的實現變得異常方便。只要在執行的方法前加一個go關鍵字就可以實現非同步操作。但是如果需求是,按照呼叫的先後順序(FIFO)來返回值我們應該怎麼辦。大家都知道,一系列的方法呼叫如果使用了非同步執行那麼就並不能保證返回的先後順序,返回的先後順序取決於每個函式耗時的長短,耗時短的則會先返回。當然解決這個問題的辦法有很多,在最近看的一本書中發現了chan巢狀chan可以很巧妙的實現這個需求。
沒解決之前
先看一下沒有使用巢狀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秒
operation2:aaa operation2:bbb operation1:ccc operation1:ddd operation2:eee Process time 2.002714923s
總結
其實解決此類問題的方法不止一個,比如在請求喝返回中新增標識等等。
但我認為這個方法巧妙的運用了chan中巢狀chan和go語言 chan阻塞的特性來實現這個功能程式碼簡潔。效能也兵沒有消耗太多,總體執行時間也並沒有加長。也是一個參考的方法,這種巢狀實現也可以用到其他需要的特殊需求中。