了解 Goroutine

已完成

並行是各種獨立活動的組成,類似於網頁伺服器同時處理多名使用者要求時所執行的工作,但是是自發性執行。 現今許多程式中都可以看見並行的影子。 網頁伺服器就是其中一例,而在批次處理大量資料的作業中,也會需要並行。

Go 提供兩種方式,讓您編寫並行程式。 其中之一種是傳統的方法,您可能曾在其他語言的執行緒使用過。 在本課程模組中,您將學習 Go 的方法,值會在稱為 goroutines 的獨立活動之間傳遞,以傳遞處理序。

若您是第一次學習並行,建議您另外花一點時間,檢閱我們將陸續撰寫的各個程式碼片段作為練習。

Go 的並行方法

一般而言,編寫並行程式的最大問題,在處理序之間的資料共用。 Go 採用的通訊方法,與其他程式設計語言不同,因為 Go 透過通道來回傳遞資料。 這個方法意指只有一項活動 (Goroutine) 可以存取資料,而且根據設計不會出現任何競爭狀況。 當您了解本課程模組中的 Goroutine 與通道後,您將會更了解 Go 處理並行的方法。

Go 的方法可以由下列語句摘要說明 :「請勿透過共用記憶體進行通訊; 反之,藉由通訊來共用記憶體。」 我們將在下列章節中討論這種方法,但您也可以在 Go 部落格文章 透過通訊共用記憶體 來深入了解。

一如稍早所言,Go 也包含低層級的並行基本類型。 但我們只會討論本課程模組中適用於並行的 Go 慣用方法。

讓我們從探索 Goroutine 開始。

Goroutine

Goroutine 是輕量執行緒中的並行活動,而不是作業系統中的傳統活動。 假設您有一個程式會寫入輸出,另有一個函數會執行計算 (例如兩個數字相加)。 並行程式可有幾個 Goroutine 同時呼叫這兩個函式。

我們可以說,程式執行的第一個 Goroutine 是 main() 函式。 您若要建立另一個 Goroutine,必須在呼叫函式之前使用 go 關鍵字,如下所示:

func main(){
    login()
    go launch()
}

或者,您會發現許多程式偏好使用匿名函式來建立 Goroutine,如這個程式碼中:

func main(){
    login()
    go func() {
        launch()
    }()
}

讓我們撰寫並行程式,看看 goroutines 實際運作的狀況。

撰寫並行程式

因為我們只想著重在並行部分,所以我們要使用現有的程式,檢查 API 端點是否有回應。 程式碼如下:

package main

import (
    "fmt"
    "net/http"
    "time"
)

func main() {
    start := time.Now()

    apis := []string{
        "https://management.azure.com",
        "https://dev.azure.com",
        "https://api.github.com",
        "https://outlook.office.com/",
        "https://api.somewhereintheinternet.com/",
        "https://graph.microsoft.com",
    }

    for _, api := range apis {
        _, err := http.Get(api)
        if err != nil {
            fmt.Printf("ERROR: %s is down!\n", api)
            continue
        }

        fmt.Printf("SUCCESS: %s is up and running!\n", api)
    }

    elapsed := time.Since(start)
    fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
}

當您執行上述程式碼時,會得到如下的輸出:

SUCCESS: https://management.azure.com is up and running!
SUCCESS: https://dev.azure.com is up and running!
SUCCESS: https://api.github.com is up and running!
SUCCESS: https://outlook.office.com/ is up and running!
ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://graph.microsoft.com is up and running!
Done! It took 1.658436834 seconds!

雖然這裡沒有出現什麼異常,但我們可以做得更好。 或許我們可以同時檢查所有網站? 這個程式只需要不到 500 毫秒就能完成,不再需要將近 2 秒鐘的時間。

請注意,需要並行執行的程式碼部分,才會對網站發出 HTTP 呼叫。 換言之,我們必須為程式所要檢查的每一個 API 建立 Goroutine。

若要建立 Goroutine,必須在呼叫函式之前,使用 go 關鍵字, 但我們還沒有任何函式。 我們可以重構該程式碼,建立新的函式,如下所示:

func checkAPI(api string) {
    _, err := http.Get(api)
    if err != nil {
        fmt.Printf("ERROR: %s is down!\n", api)
        return
    }

    fmt.Printf("SUCCESS: %s is up and running!\n", api)
}

請注意,因為我們不在 for 迴圈中,所以不再需要 continue 關鍵字。 要停止該函式的執行流程,我們使用 return 關鍵字。 現在,我們需要修改 main() 函式中的程式碼,為每個 API 建立 Goroutine,如下所示:

for _, api := range apis {
    go checkAPI(api)
}

重新執行程式,看看會產生什麼結果。

看來程式不檢查 API 了,對吧? 您應該會看到如下的輸出:

Done! It took 1.506e-05 seconds!

超快! 發生什麼事? 您會看到最後一則訊息,指出程式已完成,因為 Go 為迴圈內的每個網站建立了 goroutine,並立即進入下一行。

儘管 checkAPI 函式看起來像沒有執行,但實際上卻是有在執行, 只是來不及完成。 看看若是在迴圈之後加入睡眠計時器,將會產生什麼結果?如下所示:

for _, api := range apis {
    go checkAPI(api)
}

time.Sleep(3 * time.Second)

現在當您重新執行程式時,可能會出現如下的輸出:

ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://api.github.com is up and running!
SUCCESS: https://management.azure.com is up and running!
SUCCESS: https://dev.azure.com is up and running!
SUCCESS: https://outlook.office.com/ is up and running!
SUCCESS: https://graph.microsoft.com is up and running!
Done! It took 3.002114573 seconds!

看似正常運作對吧? 可惜不盡然。 若您想要將網站新增至清單中,該怎麼做? 3 秒鐘的時間或許不太夠。 您如何「預知」? 你無此權限。 確實還有更好的方法,我們將會在下一節討論通道時一併探討。