0%

Go Concurrency:Goroutine、Channel、WaitGroup、Mutex 一次搞懂!

在寫程式時,你是否曾遇過這樣的問題:

  • 程式跑得很慢,某些函式明明可以同時執行,卻得一個等一個?
  • 想要讓多個工作並行,但又怕不同的程序互相干擾?
  • 處理龐大的計算或 I/O 任務時,CPU 占用率極低,白白浪費資源?

這時候,你需要的是 並發(Concurrency)!而 Go 語言提供了一種超級簡單且高效的並發工具: Goroutine

在 Go 語言中,並發是其核心特性之一,而 Goroutine 則是實現並發的關鍵。你可以把它想像成一個「輕量級的執行緒」,但它比傳統執行緒更高效、佔用更少資源,並且能夠輕鬆地創建成千上萬個 Goroutine。

Goroutine 是什麼?為什麼它這麼強?

Goroutine 是 Go 語言的 輕量級執行緒(Lightweight Thread),讓你可以輕鬆執行並發程式,而不需要手動管理繁瑣的系統執行緒。

  • 輕量級:相比於傳統的作業系統執行緒(Thread),Goroutine 的記憶體開銷極低。
  • 執行簡單:只要在函式前加上 go,就能讓它跑在背景,完全不需要設定任何 Thread!
  • 獨立運行:Goroutine 之間互不影響,即使某個 Goroutine 崩潰,也不會導致其他 Goroutine 失敗。
  • 高效:Go 運行時(runtime)會自動管理 Goroutine 的調度,避免過度依賴作業系統的執行緒管理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
"time"
)

func hello() {
fmt.Println("Hello, Goroutine!")
}

func main() {
go hello() // 啟動 Goroutine
fmt.Println("Main 函式結束")
time.Sleep(time.Second) // 等待 Goroutine 執行完成
}

輸出

1
2
Main 函式結束
Hello, Goroutine!
  • go hello() 會在背景執行,而 main() 函式會繼續往下執行。
  • 這樣我們的程式就可以同時進行兩個工作,提升效率!

⚠ 但這裡有個問題—— Goroutine 是異步執行的,main() 可能在 hello() 結束前就已執行完畢,因此 time.Sleep() 讓主執行緒暫停一下,確保 Goroutine 執行。

main 函式結束後,所有 Goroutine 也會立即結束,這就引出了下一個重要工具: WaitGroup

WaitGroup:讓主程式等 Goroutine 完成,確保 Goroutine 全部完成

當我們開啟多個 Goroutine 時,main() 可能還沒等 Goroutine 執行完就結束了,導致 Goroutine 被強制終止。為了解決這個問題,我們可以使用 sync.WaitGroup

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
package main

import (
"fmt"
"sync"
"time"
)

var wg sync.WaitGroup

func printMessage(message string) {
defer wg.Done() // Goroutine 結束時通知 WaitGroup
for i := 0; i < 5; i++ {
fmt.Println(message, i)
time.Sleep(time.Millisecond * 500)
}
}

func main() {
wg.Add(2) // 設定有 2 個 Goroutine
go printMessage("Hello from Goroutine 1")
go printMessage("Hello from Goroutine 2")

wg.Wait() // 等待所有 Goroutine 結束
fmt.Println("All Goroutines Finished!")
}
  • wg.Add(2) 告訴 WaitGroup 需要等 2 個 Goroutine。 <- 增加計數器
  • wg.Done() 會在 Goroutine 執行結束時通知 WaitGroup。 <- 減少計數器
  • wg.Wait()main() 等待所有 Goroutine 結束後才繼續。 <- 等待計數器歸零

這樣就能確保主程式不會提前結束!
我們再看一個範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
"sync"
"time"
)

func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // 告知 WaitGroup 這個 Goroutine 完成
fmt.Printf("Worker %d 開始工作\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d 完成工作\n", id)
}

func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait() // 等待所有 Goroutine 結束
}

Channel:Goroutine 之間的溝通管道

Goroutine 之間如何傳遞資料呢?我們可以使用 Channel,它就像是一條輸送帶,負責在 Goroutine 之間傳遞訊息,確保 Goroutine 之間安全地交換資料

Channel 的基本語法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
ch := make(chan string) // 創建 Channel

go func() {
ch <- "Hello from Goroutine!" // 發送訊息到 Channel
}()

message := <-ch // 從 Channel 接收訊息
fmt.Println(message)
}

輸出:

1
Hello from Goroutine!

另一種寫法(匿名函式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func sendMessage(ch chan string) {
ch <- "Hello from Goroutine!" // 發送訊息
}

func main() {
ch := make(chan string) // 建立 Channel
go sendMessage(ch)
message := <-ch // 從 Channel 接收訊息
fmt.Println(message)
}
  • ch := make(chan string) 建立了一個 string 類型的 Channel。
  • ch <- "Hello" 讓 Goroutine 發送訊息。
  • <-ch 讓主程式接收訊息。

這兩種寫法的主要差異在於:

  1. 第一種寫法:將 sendMessage 定義為一個獨立函式,這樣的方式有助於程式的結構化,使程式碼更具可讀性和可維護性。
  2. 第二種寫法:使用匿名函式(go func() {})直接啟動 Goroutine,這種方式適合用於簡單的情境,例如臨時的併發操作,而不需要額外定義函式。

在選擇哪種方式時,通常如果該 Goroutine 需要重複使用,則應該定義成獨立函式;如果只是一次性操作,則可以使用匿名函式。

Buffered Channel(有緩衝的 Channel)

Buffered channel 允許 goroutine 發送多個訊息而不會馬上被接收:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
ch := make(chan int, 3) // 緩衝區大小為 3

ch <- 1
ch <- 2
ch <- 3

fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
}

這樣的 Channel 允許存入 3 個數據而不會阻塞 Goroutine。
緩衝區滿了後,goroutine 會阻塞直到有空間可用。

Mutex:避免 Goroutine 同時修改資料

當多個 Goroutine 需要共享同一個變數時,可能會發生 競爭條件(Race Condition),導致不一致的結果。這時候就需要 Mutex(互斥鎖) 來確保資料安全。

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
package main

import (
"fmt"
"sync"
)

var (
counter int
lock sync.Mutex
)

func increment() {
lock.Lock() // 加鎖
counter++
lock.Unlock() // 解鎖
}

func main() {
var wg sync.WaitGroup
wg.Add(1000)

for i := 0; i < 1000; i++ {
go func() {
defer wg.Done()
increment()
}()
}

wg.Wait()
fmt.Println("Final Counter:", counter)
}

increment() 用於增加計數器,使用 mutex.Lock()mutex.Unlock() 保護 counter 變數。main() 啟動多個 Goroutine,每個 Goroutine 都會調用 increment() 。使用 sync.Mutex 可以確保 counter 的值正確。

  • lock.Lock() 確保只有一個 Goroutine 能修改 counter
  • lock.Unlock() 讓其他 Goroutine 繼續執行。

這樣就能保證並發環境下數據的一致性!

結論

Goroutine 讓 Go 的並發變得輕鬆簡單,但為了讓多個 Goroutine 之間能夠順利協作,我們需要:

  • WaitGroup 讓主程式等 Goroutine 執行完。
  • Channel 讓 Goroutine 之間安全地交換數據。
  • Mutex 避免多個 Goroutine 同時修改共享變數。