前言
大多程式都可以採取「並行 Concurrency」的方式來達成簡單安全且高效的運行,而隨著多核 CPU 出現以及特定場景對於效率的追求「平行 Parallelism」運行程式是另一個提高運行效率方向。本文練習透過 Go 複習平行程式開發下資料共享相關技巧:
- 原子操作 Atomic
- 鎖 Mutex
- 信號量 Semaphore
競爭危害
程式輸出依賴於不受控制的事件順序或時機,通常發生在平行同時存取或修改共享資源時
舉例以下程式不太可能每次 count 結果都相同,原因是因為 count++ 其實是三個步驟:
- 讀取
count - 加一
- 寫回
count
var count int
var wg sync.WaitGroup
for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() count++ }()}
wg.Wait()原子操作 Atomic
如果把產生競爭危害的程式片段改為無法分割的「原子操作」就不存在操作的先後順序,自然就不存在競爭危害:
import "sync/atomic"
var count int64
atomic.AddInt64(&count, 1)- 特點
- 開銷最輕量,效能最好
- 僅適用於「單一變數操作」
- 使用時機
- 計數器(requests count)
- 狀態標記(flag)
- 指標替換(unsafe.Pointer)
鎖 Mutex
import "sync"
var mu sync.Mutexvar count int
mu.Lock()count++ // 臨界區mu.Unlock()- 特點
- 可保護任意複雜邏輯
- 易理解、通用性高
- 有鎖競爭成本
- 使用時機
- 複雜狀態更新
- 複雜資料操作(struct / map)
- 需要「臨界區」保護
信號量 Semaphore
sem := make(chan struct{}, 3) // 最多 3 個同時執行
for i := 0; i < 10; i++ { go func() { sem <- struct{}{} // acquire defer func() { <-sem }() // release
// do work }()}- 特點
- 控制「同時執行數量」
- 不直接保護資料
- 常用於資源限制
- 使用時機
- API rate limit
- DB connection pool
- worker pool 控制
具狀態的 Goroutine
package main
import ( "fmt" "sync" "sync/atomic")
type op struct { action string // "deposit" | "withdraw" | "balance" amount int resp chan result}
type result struct { balance int ok bool}
func bankManager(ops chan op) { balance := 0 for o := range ops { switch o.action { case "deposit": balance += o.amount o.resp <- result{balance: balance, ok: true} case "withdraw": if balance >= o.amount { balance -= o.amount o.resp <- result{balance: balance, ok: true} } else { o.resp <- result{balance: balance, ok: false} } case "balance": o.resp <- result{balance: balance, ok: true} } }}
func main() { ops := make(chan op) go bankManager(ops) // 唯一擁有 balance 的 goroutine
var wg sync.WaitGroup var successCount uint64
// 模擬 5 個用戶同時操作 actions := []struct { action string amount int user string }{ {"deposit", 1000, "Alice"}, {"deposit", 500, "Bob"}, {"withdraw", 200, "Alice"}, {"withdraw", 9999, "Bob"}, // 餘額不足 {"balance", 0, "Charlie"}, }
for _, a := range actions { wg.Add(1) a := a go func() { defer wg.Done() resp := make(chan result, 1) ops <- op{action: a.action, amount: a.amount, resp: resp} r := <-resp if r.ok { atomic.AddUint64(&successCount, 1) fmt.Printf("[%s] %-8s %4d → 餘額: %d\n", a.user, a.action, a.amount, r.balance) } else { fmt.Printf("[%s] %-8s %4d → 失敗,餘額不足(現有: %d)\n", a.user, a.action, a.amount, r.balance) } }() }
wg.Wait() close(ops) fmt.Printf("\n成功操作次數:%d\n", successCount)}- 特點
- 狀態由單一 goroutine 獨佔,其他 goroutine 透過 channel 傳遞請求
- 存取狀態的瞬間是排序執行的,但其餘業務邏輯仍並行執行
- 無需顯式管理鎖
- 邏輯較複雜,但正確性更容易推理
- 使用時機
- 狀態需要集中管理,且已有多個 channel 在協作
- 管理多把 Mutex 容易出錯且難以維護時
通訊共享記憶體還是共享記憶體通訊?
Go 雖然有個座右銘叫做:「透過通訊共享記憶體,而不是透過共享記憶體通訊」,不過並不強迫使用特定程式風格來實踐
- Mutex 方案:任何 goroutine 都可直接碰資料,靠「鎖」來排隊
- Stateful Goroutine:資料只有一個主人,其他人只能「通訊」
延伸閱讀
- Go Wiki: Use a sync.Mutex or a channel? - Go
- Atomic Counters - Go by Example
- Stateful Goroutines - Go by Example
- Go 非同步運算 (Goroutine) - WebDong