下次想在Golang中寫個併發處理,就用這個模板,準沒錯!

jeremy1127發表於2021-05-07

要說Golang中最引以為傲的特性,那非goroutine莫屬,goroutine(協程)很輕量,相比於每個執行緒要使用1MB的記憶體,每個go協程只需要1kb左右就夠了。

於是,在需要做併發處理的時候,很自然的就想著,go一下就好了嗎? 示例程式碼如下

    for i:=0; i < 5; i++ {
        go func(index int) {
            fmt.Println(index)
        }(i) //這裡為什麼要把i傳進來呢?
    }

這樣可以併發處理請求了是不假,但如果其中一個請求出錯了,需要退出怎麼辦了? 一方面,可以自己實現這個錯誤處理(稍後會寫),另一方面,也可以直接用golang官方errorgroup

上面的示例程式碼,如果用errorgroup來重新實現,會是下面這個樣子

    g, _ := errgroup.WithContext(context.Background())

    for i := 0; i < 5; i++ {
        index := i
        g.Go(func() error {
            fmt.Println(index)
            return nil // 如果想Mock一些錯誤,也可以return一個error
        })
    }

    if err = g.Wait(); err != nil {
        return err
    }

是不是還挺簡單的?感興趣的,可以自行搜下原始碼,除去註釋只有大概30行程式碼,還是很好理解的。

現在錯誤處理也有了,是不是就完美了呢?

這個問題就要看你併發處理多少請求了,協程雖然很輕量,但也還是要耗費一些資源的,所以如果可以預見到有幾百上千的請求的要處理,那就需要協程池來複用協程,達到節省資源的目的了。

網上有很多協程池的實現,大都做的大而全,考慮了很多場景,但實際編碼場景中,很可能只是為了解決一個小問題,就引入一個包,實在覺得有些太重了呢,而且可能還不夠靈活。

有沒有一個簡單的模板,可以copy/paste/tweak一下呢?這就來了

閒話少絮,直接上程式碼先,關鍵部分會在程式碼中加註釋解釋。

    var (
        err         error
        outputs     []int
        workers     = 4 //協程的數量,可以按需設定,一般不大於runtime.NumCPU()
        workChannel = make(chan int)
        errChannel  = make(chan error, workers)
        wg          = &sync.WaitGroup{}
        mux         = sync.Mutex{}
    )

    worker := func(input int) (int, error){
        retrun input, nil //如果想Mock一些錯誤,也可以return一個error
    }

    wg.Add(workers)
    for i := 0; i < workers; i++ {
        go func() {
            defer wg.Done()
            for input := range workChannel { // workChannel被close時,這個迴圈就會退出
                output, err:=worker(input)
                if err != nil {
                    errChannel <- err
                    break
                }

                mux.Lock() //使用lock保護outputs,來蒐集執行結果,如果不需要可以刪除
                outputs = append(outputs, output) 
                mux.Unlock()
            }
        }()
    }

loop:
    for _, input := range inputs {
        select {
        case workChannel <- input:
        case err = <-errChannel:
            break loop
        }
    }

    close(workChannel) //關閉workChannel,可以讓工作協程,在處理完當前任務後退出
    wg.Wait()

    // 以於select case,如果有多個case滿足時,會選擇隨機進入一個case的,所以需要再檢查一次,雙重保險
    if err == nil {
        select {
        case err = <-errChannel:
        default:
        }
    }

    return outputs, err

程式碼看著是多了些,但在實際使用過程中,按需要改下worker函式輸入和輸出的型別即可。

如果copy/paste也不想做,那就只能封裝一下了,但是因為現在golang還沒正式推出範型,只能用inteface{}了,看著是不大好看,使用時,也要自己轉來轉去的,不過可以湊合著用啦。

話說,封裝好的程式碼在這 parallel_runner.go,需要的自取了。

也許有人會問,為什麼不用context.WithCancel(),然後在出現錯誤的時候cancel一下?
講究一點的話,確實應該用,但那也意味在在worker函式中,你也要檢查ctx.Done(), 我不用還是因為懶了……

之前還寫過一些關於channel的使用的文章,

裡面實現的輕量級util都開源在channelx,歡迎大家審閱,如果有你喜歡用的工具,歡迎點個贊或者star :)

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章