了解 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 秒鐘的時間或許不太夠。 您如何「預知」? 你無此權限。 確實還有更好的方法,我們將會在下一節討論通道時一併探討。