Golang 併發程式設計實踐

K8sCat發表於2021-04-22

人是一種高併發的物種,細品。

初識

對 Go 語言的第一印象就是其原生地支援併發程式設計,而且使用的是協程,比執行緒更加輕量。

關於程式、執行緒和協程的區別

  • 程式是“程式執行的一個例項” ,擔當分配系統資源的實體。程式建立必須分配一個完整的獨立地址空間。程式切換隻發生在核心態。
  • 執行緒:執行緒是程式的一個執行流,獨立執行它自己的程式程式碼,是程式執行流的最小單元,是處理器排程和分派的基本單位。一個程式可以有一個或多個執行緒。
  • 協程:協程不是程式或執行緒,其執行過程更類似於子例程,或者說不帶返回值的函式呼叫。在語言級別可以建立併發協程,然後編寫程式碼去進行管理。Go 將這一步承包下來,使協程併發執行成本更低。

Go 實現最簡單的併發

for i := 0; i < 10; i++ {
    go func(n int) {
        fmt.Println(n)
    }(i)
}

專案實踐

最近有個專案需要同時呼叫多個 job,並要等待這些 job 完成之後才能往下執行。

序列執行 job

最開始,我們擁有一個執行 job 的方法,並且序列執行所有的 job:

func buildJob(name string) {
    ...
}

buildJob("A")
buildJob("B")
buildJob("C")

並行執行 job

因為所有 job 是可以併發執行的,這樣就不用必須等待上一個 job 執行完成後,才能繼續執行其他 job。我們可以使用 Go 語言的關鍵字 go 來快速啟用一個 goroutine,下面我們將併發地執行三個 job:

go buildJob("A")
go buildJob("B")
go buildJob("C")

等待所有 job 完成

怎樣才能知道每個 job 是否已經完成,這裡可以使用 channel 進行通訊,並使用 select 檢查執行結果:

func buildJob(ch chan error, name string) {
    var err error

    ... // build job

    ch <- err // finnaly, send the result into channel
}

func build() error {
    jobCount := 3
    errCh := make(err chan error, jobCount)
    defer close(errCh) // 關閉 channel

    go buildJob(errCh, "A")
    go buildJob(errCh, "B")
    go buildJob(errCh, "C")

    for {
        select {
        case err := <-ch:
            if err != nil {
                return err
            }
        }

        jobCount--
        if jobCount <= 0 {
            break
        }
    }

    return nil
}

發現問題

當 job A 執行失敗時,build 方法會 return err 退出,並執行 close(errCh)。可是此時另外兩個 job B 和 C 可能還沒執行完成,同時也會把結果發給 errCh,但由於這個時候 errCh 已經被關閉了,會導致程式退出 panic: send on closed channel

優化程式碼

在給 channel 傳送資料的時候,可以使用接收資料的第二個值判斷 channel 是否關閉:

func buildJob(ch chan error, name string) {
    var err error

    ... // build job

    if _, ok := <-ch; !ok {
        return
    }
    ch <- err // finnaly, send the result into channel
}

func build() error {
    jobCount := 3
    errCh := make(err chan error, jobCount)
    defer close(errCh) // 關閉 channel

    go buildJob(errCh, "A")
    go buildJob(errCh, "B")
    go buildJob(errCh, "C")

    for {
        select {
        case err := <-ch:
            if err != nil {
                return err
            }
        }

        jobCount--
        if jobCount <= 0 {
            break
        }
    }

    return nil
}

總結

Go 併發程式設計看似只需要一個關鍵字 go 就可以跑起來一個 goroutine,但真正實踐中,還是有需要問題需要去處理的。

原文連結:k8scat.com/posts/code-with-golang-...

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

相關文章