
上一篇,我們學會了如何使用 goroutine 來實現併發,並用 WaitGroup 協調它們。
但是呢,單純的併發執行會帶來一個嚴重的問題:多個 goroutine 同時讀寫同一個變數,會導致數據不一致。
今天會探討怎麼保護共享記憶體,並介紹 Go 語言的併發工具:Mutex 和 RWMutex。
當多個 goroutine 同時讀寫同一個記憶體變數時,就會發生競爭條件。sync.Mutex (互斥鎖) 是解決這個問題的工具。
它像一扇只有一把鑰匙的門。
任何 goroutine 想要進入被保護的程式碼區塊(臨界區),都必須先獲得鎖 (Lock)。
執行完畢後,必須釋放鎖 (Unlock),下一個等待的 goroutine 才能進入。
// 位於 Day3/mutex/main.go
var (
	counter int
	mu      sync.Mutex // 保護 counter 的鎖
)
func safeIncrement(wg *sync.WaitGroup) {
    defer wg.Done()
    for i := 0; i < 1000; i++ {
        mu.Lock()   // 取得鎖
        counter++   // --- 臨界區開始 ---
        mu.Unlock() // --- 臨界區結束 --- 釋放鎖
    }
}
這確保了 counter++ 操作的原子性,最後的準確結果。

Mutex 雖然能保證資料安全,但它是有成本的。
它透過強制序列化來解決衝突——一次只允許一個 goroutine 進入臨界區。
在高競爭的場景下,這會導致大量的 goroutine 阻塞和等待,從而嚴重影響程式的並行效能。
它犧牲了並行性來換取安全性。
把它看作是一種必要的惡,一種在沒有更好選擇時才使用的「蠻力」手段。

到目前為止,我們已經有了 goroutine、WaitGroup 和 Mutex 三個獨立的工具。
現在將它們組合起來而且解決一個實際問題:如何構建一個允許多個 goroutine 同時操作,但結果依然準確的計數器?
這裡也會用到封裝 (Encapsulation) 的思想。
把你「需要保護的資料 value」  和「用來保護它的鎖mu」 包在同一個結構體 (struct) 裡面。
這樣任何外部程式碼都不能直接觸碰 value。你必須透過我們定義好的方法 (Increment, Value) 來存取它,而這些方法內部已經幫你處理好了加鎖和解鎖的邏輯。
這極大地降低了出錯的風險。你不可能會忘記加鎖,因為你根本沒有機會直接操作資料。
goroutine, WaitGroup, Mutex 的協同工作// 位於 Day3/example/main.go
package main
import (
	"fmt"
	"sync"
)
// SafeCounter 是一個併發安全的計數器
// 它「封裝」了計數值和一個互斥鎖
type SafeCounter struct {
	mu    sync.Mutex
	value int
}
// Increment 會安全地對計數器加一
func (c *SafeCounter) Increment() {
	// Mutex: 在修改 value 前,鎖定計數器,防止其他 goroutine 介入
	c.mu.Lock()
	defer c.mu.Unlock()
	c.value++
}
// Value 會安全地讀取計數器的值
func (c *SafeCounter) Value() int {
	// Mutex: 即使是讀取,也要鎖定,以防止讀到一個正在被修改的「髒」資料
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.value
}
// worker 代表一個獨立的併發任務
func worker(id int, counter *SafeCounter, wg *sync.WaitGroup) {
	// WaitGroup: 確保在 worker 函式結束時,通知 WaitGroup 任務已完成
	defer wg.Done()
	for i := 0; i < 100; i++ {
		counter.Increment()
	}
	fmt.Printf("Worker %d 完成了 100 次計數\n", id)
}
func main() {
    fmt.Println("=== 三元組組合技展示 ===")
 
	counter := &SafeCounter{}
	var wg sync.WaitGroup
	// 我們要啟動 10 個併發任務
	numWorkers := 10
	for i := 1; i <= numWorkers; i++ {
		// WaitGroup: 每啟動一個 worker,計數器就加 1
		wg.Add(1)
		// goroutine: 使用 'go' 關鍵字,讓每個 worker 在獨立的 goroutine 中併發執行
		go worker(i, counter, &wg)
	}
	// WaitGroup: 阻塞主程式,直到所有 worker 都呼叫了 Done(),計數器歸零
	wg.Wait()
 
    fmt.Printf("最終計數結果:%d\n", counter.Value())
    fmt.Printf("預期結果:1000(10個worker × 100次)\n")
}
執行結果:
goroutine, WaitGroup, Mutex 的協同工作
sync.RWMutex雖然 Mutex 很好用,但它有一個限制:即使是讀取操作,也必須等待寫入鎖釋放。
在讀取頻繁但寫入較少的場景下,Mutex 的效能瓶頸會非常明顯。
這時,我們可以使用更精細的工具:sync.RWMutex (讀寫鎖)。
RLock(): 取得讀取鎖,允許多個 goroutine 同時讀取。
RUnlock(): 釋放讀取鎖。
Lock(): 取得寫入鎖,會阻塞所有讀取和寫入操作。
Unlock(): 釋放寫入鎖。
使用 RWMutex 重構 SafeCounter 的 Value 方法:
// SafeCounterRWMutex 是一個使用讀寫鎖的併發安全計數器
type SafeCounterRWMutex struct {
	mu      sync.RWMutex
	value   int
}
// Increment 依然使用寫入鎖,確保修改時沒有其他操作
func (c *SafeCounterRWMutex) Increment() {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.value++
}
// Value 使用讀取鎖,允許多個讀取同時發生
func (c *SafeCounterRWMutex) Value() int {
	c.mu.RLock()
	defer c.mu.RUnlock()
	return c.value
}
這個小小的改變,卻能大幅提升程式在讀多寫少場景下的並行效能。
如何保護共享記憶體:
Mutex 是保護共享記憶體的「蠻力」工具,它透過犧牲並行性來保證資料一致性。
RWMutex 是 Mutex 的進階版,能優化讀多寫少場景的效能。
封裝:將資料和其對應的鎖封裝在一個結構體內,是推薦的實踐方式。
Mutex 是解決競爭條件的基礎手段,但透過鎖來保護共享資源的方式有效能上的代價。
這讓我們去尋找更優雅的方案,而 Go 語言更推崇另一種併發模型——Channel。下篇將繼續探討。
Mutex in Golang With Examples - GeeksforGeeks:解釋 sync.Mutex 如何防止競態條件
Go by Example: Mutexes:簡單明瞭的程式碼範例可以快速掌握 Mutex 的基本用法。
Go Concurrency Patterns:這是 Go 語言創始人 Rob Pike 的經典演講,解釋了 Go 語言的併發哲學,對理解為什麼 Go 更推崇 Channel 而非傳統鎖有很大幫助。