Go 語言 sync 包使用教程
Go 語言的 sync 包提供了基本的同步原語,用于在并發編程中協調 goroutine 之間的操作。
1. 互斥鎖 (Mutex)
互斥鎖用于保護共享資源,確保同一時間只有一個 goroutine 可以訪問。
特點:
- 最基本的同步原語,實現互斥訪問共享資源
- 有兩個方法:
Lock()
和Unlock()
- 不可重入,同一個 goroutine 重復獲取會導致死鎖
- 沒有超時機制,鎖定后必須等待解鎖
- 不區分讀寫操作,所有操作都是互斥的
- 適用于共享資源競爭不激烈的場景
- 性能高于 channel 實現的互斥機制
- 不保證公平性,可能導致饑餓問題
import ("fmt""sync""time"
)func main() {var mutex sync.Mutexcounter := 0for i := 0; i < 1000; i++ {go func() {mutex.Lock()defer mutex.Unlock()counter++}()}time.Sleep(time.Second)fmt.Println("計數器:", counter)
}
2. 讀寫鎖 (RWMutex)
當多個 goroutine 需要讀取而很少寫入時,讀寫鎖比互斥鎖更高效。
特點:
- 針對讀多寫少場景優化的鎖
- 提供四個方法:
RLock()
、RUnlock()
、Lock()
、Unlock()
- 允許多個讀操作并發進行,但寫操作是互斥的
- 寫鎖定時,所有讀操作都會被阻塞
- 有讀鎖定時,寫操作會等待所有讀操作完成
- 寫操作優先級較高,防止寫饑餓
- 內部使用 Mutex 實現
- 比 Mutex 有更多開銷,但在讀多寫少場景下性能更高
var rwMutex sync.RWMutex
var data map[string]string = make(map[string]string)// 讀取操作
func read(key string) string {rwMutex.RLock()defer rwMutex.RUnlock()return data[key]
}// 寫入操作
func write(key, value string) {rwMutex.Lock()defer rwMutex.Unlock()data[key] = value
}
3. 等待組 (WaitGroup)
等待組用于等待一組 goroutine 完成執行。
特點:
- 用于協調多個 goroutine 的完成
- 提供三個方法:
Add()
、Done()
、Wait()
Add()
增加計數器,參數可為負數Done()
等同于Add(-1)
,減少計數器Wait()
阻塞直到計數器歸零- 計數器不能變為負數,會導致 panic
- 可以重用,計數器歸零后可以再次增加
- 非常適合"扇出"模式(啟動多個工作 goroutine 并等待全部完成)
- 不包含工作內容信息,僅表示完成狀態
- 輕量級,開銷很小
func main() {var wg sync.WaitGroupfor i := 0; i < 5; i++ {wg.Add(1) // 增加計數器go func(id int) {defer wg.Done() // 完成時減少計數器fmt.Printf("工作 %d 完成\n", id)}(i)}wg.Wait() // 等待所有 goroutine 完成fmt.Println("所有工作已完成")
}
4. 一次性執行 (Once)
Once 確保一個函數只執行一次,無論有多少 goroutine 嘗試執行它。
特點:
- 確保某個函數只執行一次
- 只有一個方法:
Do(func())
- 即使在多個 goroutine 中調用也只執行一次
- 常用于單例模式或一次性初始化
- 內部使用互斥鎖和一個標志位實現
- 非常輕量級,幾乎沒有性能開銷
- 如果傳入的函數 panic,視為已執行
- 不能重置,一旦執行就不能再次執行
- 傳入不同的函數也不會再次執行
var once sync.Once
var instance *singletonfunc getInstance() *singleton {once.Do(func() {instance = &singleton{}})return instance
}
5. 條件變量 (Cond)
條件變量用于等待或宣布事件的發生。
特點:
- 用于等待或通知事件發生
- 需要與互斥鎖結合使用:
sync.NewCond(&mutex)
- 提供三個方法:
Wait()
、Signal()
、Broadcast()
Wait()
自動解鎖并阻塞,被喚醒后自動重新獲取鎖Signal()
喚醒一個等待的 goroutineBroadcast()
喚醒所有等待的 goroutine- 適合生產者-消費者模式
- 可以避免輪詢,提高性能
- 使用相對復雜,容易出錯
- 等待必須在獲取鎖后調用
var mutex sync.Mutex
var cond = sync.NewCond(&mutex)
var ready boolfunc main() {go producer()// 消費者mutex.Lock()for !ready {cond.Wait() // 等待條件變為真}fmt.Println("數據已準備好")mutex.Unlock()
}func producer() {time.Sleep(time.Second) // 模擬工作mutex.Lock()ready = truecond.Signal() // 通知一個等待的 goroutine// 或使用 cond.Broadcast() 通知所有等待的 goroutinemutex.Unlock()
}
6. 原子操作 (atomic)
對于簡單的計數器或標志,可以使用原子操作包而不是互斥鎖。
特點:
- 底層的原子操作,無鎖實現
- 適用于簡單的計數器或標志位
- 比互斥鎖性能更高,開銷更小
- 提供多種原子操作:
Add
、Load
、Store
、Swap
、CompareAndSwap
- 支持多種數據類型:int32、int64、uint32、uint64、uintptr 和指針
- 可用于實現自己的同步原語
- 不適合復雜的共享狀態
- 在多 CPU 系統上可能導致緩存一致性開銷
- Go 1.19 引入了新的原子類型
import ("fmt""sync/atomic""time"
)func main() {var counter int64 = 0for i := 0; i < 1000; i++ {go func() {atomic.AddInt64(&counter, 1)}()}time.Sleep(time.Second)fmt.Println("計數器:", atomic.LoadInt64(&counter))
}
7. Map (sync.Map)
Go 1.9 引入的線程安全的 map。
特點:
- Go 1.9 引入的線程安全的哈希表
- 無需額外加鎖即可安全地并發讀寫
- 提供五個方法:
Store
、Load
、LoadOrStore
、Delete
、Range
- 內部使用分段鎖和原子操作優化性能
- 適用于讀多寫少的場景
- 不保證遍歷的順序
- 不支持獲取元素數量或判斷是否為空
- 不能像普通 map 那樣直接使用下標語法
- 性能比加鎖的普通 map 更好,但單線程下比普通 map 慢
- 內存開銷較大
var m sync.Mapfunc main() {// 存儲鍵值對m.Store("key1", "value1")m.Store("key2", "value2")// 獲取值value, ok := m.Load("key1")if ok {fmt.Println("找到鍵:", value)}// 如果鍵不存在則存儲m.LoadOrStore("key3", "value3")// 刪除鍵m.Delete("key2")// 遍歷所有鍵值對m.Range(func(key, value interface{}) bool {fmt.Println(key, ":", value)return true // 返回 false 停止遍歷})
}
8. Pool (sync.Pool)
對象池用于重用臨時對象,減少垃圾回收壓力。
特點:
- 用于緩存臨時對象,減少垃圾回收壓力
- 提供兩個方法:
Get()
和Put()
- 需要提供
New
函數來創建新對象 - 對象可能在任何時候被垃圾回收,不保證存活
- 在 GC 發生時會清空池中的所有對象
- 不適合管理需要顯式關閉的資源(如文件句柄)
- 適合于頻繁創建和銷毀的對象
- 沒有大小限制,Put 總是成功的
- 每個 P(處理器)有自己的本地池,減少競爭
- Go 1.13 后大幅提升了性能
var bufferPool = sync.Pool{New: func() interface{} {return new(bytes.Buffer)},
}func process() {// 獲取緩沖區buffer := bufferPool.Get().(*bytes.Buffer)buffer.Reset() // 清空以便重用// 使用緩沖區buffer.WriteString("hello")// 操作完成后放回池中bufferPool.Put(buffer)
}
9. 綜合示例
下面是一個綜合示例,展示了多個同步原語的使用:
package mainimport ("fmt""sync""time"
)type SafeCounter struct {mu sync.Mutexwg sync.WaitGroupcount int
}func main() {counter := SafeCounter{}// 啟動 5 個 goroutine 增加計數器for i := 0; i < 5; i++ {counter.wg.Add(1)go func(id int) {defer counter.wg.Done()for j := 0; j < 10; j++ {counter.mu.Lock()counter.count++fmt.Printf("Goroutine %d: 計數器 = %d\n", id, counter.count)counter.mu.Unlock()// 模擬工作time.Sleep(100 * time.Millisecond)}}(i)}// 等待所有 goroutine 完成counter.wg.Wait()fmt.Println("最終計數:", counter.count)
}
最佳實踐
-
使用 defer 解鎖:確保即使發生錯誤也能解鎖
mu.Lock() defer mu.Unlock()
-
避免鎖的嵌套:容易導致死鎖
-
保持臨界區簡短:鎖定時間越短越好
-
基準測試比較:
- 對于大多數簡單操作,atomic 比 Mutex 快
- RWMutex 在讀操作遠多于寫操作時優于 Mutex
- sync.Map 在高并發下比加鎖的 map 性能更好
-
內存對齊:
- 原子操作需要內存對齊
- 不正確的內存對齊會嚴重影響性能
- 特別是在 32 位系統上使用 64 位原子操作
-
超時控制:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel()done := make(chan struct{}) go func() {// 執行可能耗時的操作mu.Lock()// ...mu.Unlock()done <- struct{}{} }()select { case <-done:// 操作成功完成 case <-ctx.Done():// 操作超時 }