Go | 超級簡單的學會用並行
💬簡介
Golang 的並發簡單使用:Goroutine 和 Channel。
隨著多核處理器的普及,程式語言也開始朝向並行化發展。Go 語言(Golang)在這個背景下誕生,並且原生支援並發程式設計。與許多傳統語言不同,Go 提供了一種非常簡單的方式來處理並發,開發者可以輕鬆利用多核 CPU 提升效能。
圖片來源:Gophers (地鼠造型的原創者為 Renee French)
👥並發機制:Goroutine 和 Channel
Go 的並發機制基於兩個重要的概念:goroutine 和 channel。
Goroutine:是輕量級的執行單元,類似於「輕量級的線程」。開發者只需使用 go 關鍵字啟動一個 goroutine,程式運行時會自動調度這些 goroutine 到不同的 CPU 核心上執行,開發者無需手動管理。
Channel:是用來進行 goroutine 之間通訊的管道。Channel 使得不同 goroutine 之間可以安全地交換資料,並且簡化了同步操作。資料可以透過 Channel 發送和接收,開發者無需擔心複雜的鎖或競爭條件。
📌簡單的 Goroutine 與 Channel 範例
這是最基礎的示範,展示如何使用 goroutine 和 channel 執行並發操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| package main
import ( "fmt" "time" )
func greet(name string, channel chan<- string) { for i := 1; i <= 3; i++ { time.Sleep(time.Millisecond * 500) fmt.Printf("Greet %d: Hello, %s!\n", i, name) } channel <- fmt.Sprintf("Finished greeting: %s", name) }
func main() { messageChannel := make(chan string)
go greet("Alice", messageChannel) go greet("Bob", messageChannel)
fmt.Println(<-messageChannel) fmt.Println(<-messageChannel) }
|
🖨輸出:
1 2 3 4 5 6 7 8
| Greet 1: Hello, Alice! Greet 1: Hello, Bob! Greet 2: Hello, Bob! Greet 2: Hello, Alice! Greet 3: Hello, Alice! Finished greeting: Alice Greet 3: Hello, Bob! Finished greeting: Bob
|
📌解析:
- greet 函式:在兩個 goroutine 中並行運行,每個函式模擬多次工作並將結果發送到通道。
- 主程序從通道接收兩個 goroutine 的結果並輸出,顯示它們並行運行的結果。
🚀為什麼 Golang 的並發如此簡單?
- 輕量的 goroutine:
- Goroutine 不像傳統線程那樣需要大量記憶體和開銷,每個只需幾 KB 的記憶體,使得同時運行成千上萬的 goroutine 成為可能。
- 自動調度:
- 運行時會自動將 goroutine 調度到不同的 CPU 核心,開發者無需關心執行順序或負載均衡問題,專注於業務邏輯即可。
- 簡單的通訊:
- Channel 提供直觀的資料傳遞方式,讓不同 goroutine 之間能夠進行同步,而無需擔心鎖操作。通訊模型(CSP)避免資料競爭,簡單易懂。
- 不需手動管理線程:
- 傳統多線程需要手動創建線程、管理線程池、設置鎖等,而 goroutine 和 channel 封裝了這些操作,極大簡化了並發設計。
♾️更多範例
【範例】<點擊>點擊>
範例 1:計算平方的生產者與消費者模型
這個範例展示了如何實現生產者與消費者模式,並利用 goroutine 和 channel 將數據傳遞給消費者進行處理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| package main
import ( "fmt" "time" )
func producer(channel chan<- int) { for i := 1; i <= 5; i++ { channel <- i time.Sleep(time.Second) } close(channel) }
func consumer(channel <-chan int) { for num := range channel { fmt.Printf("Square of %d is %d\n", num, num*num) } }
func main() { channel := make(chan int)
go producer(channel) consumer(channel) }
|
解析:
- 生產者每秒生產一個數字並通過通道發送給消費者。
- 消費者接收到數字後,計算並顯示其平方值。
範例 2:計算總和的並行處理
這個範例展示如何使用多個 goroutine 並行計算多個數字陣列的總和,然後將結果發送到主函式中合併。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| package main
import ( "fmt" "time" )
func sum(start, end int, result chan<- int) { total := 0 for i := start; i <= end; i++ { total += i } result <- total }
func main() { ranges := []struct{ start, end int }{ {1, 25}, {26, 50}, {51, 75}, {76, 100}, }
resultChannel := make(chan int, len(ranges))
for _, r := range ranges { go sum(r.start, r.end, resultChannel) }
total := 0 for i := 0; i < len(ranges); i++ { total += <-resultChannel }
fmt.Printf("Total sum is: %d\n", total) }
|
解析:
- 程式將數字範圍分成四段,並使用四個 goroutine 並行計算每段的總和。
- 最後,主函式從通道中接收結果並將其加總。
範例 3:並行網頁請求
這個範例展示了如何並行發送多個 HTTP 請求並處理回應。這在需要並行處理多個網頁的情境下很有用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| package main
import ( "fmt" "net/http" "time" )
func fetchURL(url string, channel chan<- string) { resp, err := http.Get(url) if err != nil { channel <- fmt.Sprintf("Error fetching %s: %v", url, err) return } defer resp.Body.Close() channel <- fmt.Sprintf("Fetched %s with status: %s", url, resp.Status) }
func main() { urls := []string{ "https://golang.org", "https://www.example.com", "https://www.wikipedia.org", }
channel := make(chan string, len(urls))
for _, url := range urls { go fetchURL(url, channel) }
for range urls { fmt.Println(<-channel) } }
|
解析:
- 每個 HTTP 請求都是在一個單獨的 goroutine 中並行發送的,然後將結果發送到通道中。
- 主函式從通道中接收回應並顯示結果。
範例 4:並發處理大量任務
這個範例演示如何利用 goroutine 並行處理大量任務,並發送任務完成的通知。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| package main
import ( "fmt" "time" )
func processTask(taskId int, channel chan<- string) { time.Sleep(time.Millisecond * 500) channel <- fmt.Sprintf("Task %d completed!", taskId) }
func main() { channel := make(chan string)
for i := 1; i <= 10; i++ { go processTask(i, channel) }
for i := 1; i <= 10; i++ { fmt.Println(<-channel) } }
|
解析:
- 我們創建了 10 個 goroutine 來處理 10 個任務,並在每個任務完成時將結果發送到通道。
- 主函式等待並顯示所有任務完成的結果。
🎯總結
Go 的並發設計簡單而強大。開發者只需使用 go 關鍵字啟動 goroutine,並透過 channel 進行通信,便能輕鬆實現並發程式。運行時自動處理線程調度和同步,讓開發者專注於業務邏輯,而無需關心底層細節。
在本篇範例中,我們展示了如何使用 goroutine 和 channel 在 Go 語言中實現並發的基本概念。雖然這些範例沒有深入到更複雜的同步機制或錯誤處理,但它們已經展示了並發程式設計的基本思路。後續會深入分析 Go 語言中的並發機制,並介紹如何處理更高級的並發場景與挑戰。
最後建議回顧一下Go | 菜鳥教學目錄,了解其章節目錄。
註:以上參考了
Go
維基百科 - Go
ithome - Go 的並發:Goroutine 與 Channel 介紹(Peter Chen)
用 10 分鐘了解 Go 語言如何從 Channel 讀取資料(小惡魔 - AppleBOY)