Go Sync 包
Go 的 sync 包提供了基本的同步原语,用于在 goroutine 之间协调对共享资源的访问。除了互斥锁和读写锁,还包括 WaitGroup、Once、Map、Cond 等实用工具。配合 sync/atomic 包的原子操作,可以构建高效的并发程序。
sync.WaitGroup
WaitGroup 用于等待一组 goroutine 完成。它维护一个内部计数器,Add 增加、Done 减少,Wait 阻塞直到计数器归零。
package main
import (
"fmt"
"sync"
"time"
)
func fetchPage(url string, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("开始抓取: %s\n", url)
time.Sleep(200 * time.Millisecond) // 模拟网络请求
fmt.Printf("完成抓取: %s\n", url)
}
func main() {
urls := []string{
"https://example.com",
"https://golang.org",
"https://github.com",
}
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go fetchPage(url, &wg)
}
wg.Wait()
fmt.Println("所有页面抓取完成")
}使用要点:Add(1) 必须在 go 语句之前调用,Done() 通常用 defer 确保执行。WaitGroup 不能复制,应传递指针。
sync.Mutex(互斥锁)
Mutex 是最基本的锁机制,保证同一时刻只有一个 goroutine 能访问受保护的代码段。
package main
import (
"fmt"
"sync"
)
// SafeCounter 使用 Mutex 保护计数器
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock() // 确保 Unlock 一定被调用
c.count++
}
func (c *SafeCounter) Get() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
func main() {
counter := &SafeCounter{}
var wg sync.WaitGroup
// 启动 1000 个 goroutine 并发递增
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment()
}()
}
wg.Wait()
fmt.Printf("最终计数: %d\n", counter.Get()) // 1000
}Lock() 获取锁,如果锁已被其他 goroutine 持有则阻塞。Unlock() 释放锁。通常使用 defer c.mu.Unlock() 确保锁在函数返回时释放。
sync.RWMutex(读写锁)
RWMutex 允许多个读操作并发执行,但写操作独占。适合读多写少的场景。
package main
import (
"fmt"
"sync"
"time"
)
// SafeMap 使用 RWMutex 保护 map
type SafeMap struct {
mu sync.RWMutex
data map[string]int
}
func NewSafeMap() *SafeMap {
return &SafeMap{data: make(map[string]int)}
}
func (m *SafeMap) Get(key string) (int, bool) {
m.mu.RLock() // 读锁:允许多个并发读
defer m.mu.RUnlock()
val, ok := m.data[key]
return val, ok
}
func (m *SafeMap) Set(key string, value int) {
m.mu.Lock() // 写锁:独占
defer m.mu.Unlock()
m.data[key] = value
}
func main() {
sm := NewSafeMap()
var wg sync.WaitGroup
// 多个读操作可以并发执行
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 3; j++ {
if val, ok := sm.Get(fmt.Sprintf("key-%d", j)); ok {
fmt.Printf("Reader %d 读取 key-%d = %d\n", id, j, val)
}
}
}(i)
}
// 写操作
for i := 0; i < 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
sm.Set(fmt.Sprintf("key-%d", id), id*100)
fmt.Printf("Writer %d 写入 key-%d = %d\n", id, id, id*100)
}(i)
}
time.Sleep(100 * time.Millisecond)
wg.Wait()
// 验证结果
for i := 0; i < 3; i++ {
key := fmt.Sprintf("key-%d", i)
val, _ := sm.Get(key)
fmt.Printf("%s = %d\n", key, val)
}
}RWMutex 的方法:Lock()/Unlock() 用于写操作,RLock()/RUnlock() 用于读操作。当有写锁持有时,新的读锁请求会阻塞;当有读锁持有时,写锁请求也会阻塞。
sync.Once
Once 确保某个操作只执行一次,即使多个 goroutine 同时调用。
package main
import (
"fmt"
"sync"
"time"
)
var (
instance *Config
once sync.Once
)
type Config struct {
DatabaseURL string
APIKey string
}
func GetConfig() *Config {
once.Do(func() {
fmt.Println("初始化配置(只执行一次)...")
time.Sleep(100 * time.Millisecond) // 模拟初始化
instance = &Config{
DatabaseURL: "postgres://localhost:5432/mydb",
APIKey: "sk-xxxx",
}
})
return instance
}
func main() {
var wg sync.WaitGroup
// 10 个 goroutine 同时请求配置
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
cfg := GetConfig()
fmt.Printf("Goroutine %d 获取配置: %s\n", id, cfg.DatabaseURL)
}(i)
}
wg.Wait()
fmt.Println("所有 goroutine 获取到相同的配置实例")
}Once.Do(fn) 保证 fn 只被执行一次。即使多个 goroutine 同时调用 Do,也只有一个会执行 fn,其他 goroutine 会阻塞直到 fn 完成。常用于单例模式和延迟初始化。
sync.Map
sync.Map 是并发安全的 map 实现,针对两种场景优化:
- 键只写入一次,但多次读取(缓存)
- 多个 goroutine 读写不同的键
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
// 存储
m.Store("name", "张三")
m.Store("age", 28)
m.Store("city", "北京")
// 读取
if val, ok := m.Load("name"); ok {
fmt.Printf("name = %v\n", val)
}
// LoadOrStore:不存在时存储,存在时返回已有值
actual, loaded := m.LoadOrStore("name", "李四")
fmt.Printf("name = %v, 是否已有: %v\n", actual, loaded)
// LoadAndDelete:读取并删除
val, deleted := m.LoadAndDelete("city")
fmt.Printf("删除 city = %v, 是否成功: %v\n", val, deleted)
// 遍历
fmt.Println("\n所有键值对:")
m.Range(func(key, value interface{}) bool {
fmt.Printf(" %v = %v\n", key, value)
return true // 返回 false 停止遍历
})
// 并发安全示例
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
key := fmt.Sprintf("key-%d", n%10) // 10 个不同的键
m.Store(key, n)
}(i)
}
wg.Wait()
fmt.Println("\n并发写入完成")
}sync.Map 不需要加锁,但在需要频繁遍历或键集变化大的场景下,Mutex + map 可能更合适。
sync.Cond(条件变量)
Cond 实现条件变量,允许 goroutine 等待某个条件成立。
package main
import (
"fmt"
"sync"
"time"
)
type Queue struct {
cond *sync.Cond
items []int
mu sync.Mutex
}
func NewQueue() *Queue {
q := &Queue{}
q.cond = sync.NewCond(&q.mu)
return q
}
func (q *Queue) Push(item int) {
q.mu.Lock()
defer q.mu.Unlock()
q.items = append(q.items, item)
q.cond.Signal() // 唤醒一个等待的 goroutine
}
func (q *Queue) Pop() int {
q.mu.Lock()
defer q.mu.Unlock()
// 等待队列非空
for len(q.items) == 0 {
q.cond.Wait() // 释放锁并等待,被唤醒后重新获取锁
}
item := q.items[0]
q.items = q.items[1:]
return item
}
func main() {
q := NewQueue()
// 消费者
go func() {
for i := 0; i < 5; i++ {
val := q.Pop()
fmt.Printf("消费: %d\n", val)
time.Sleep(100 * time.Millisecond)
}
}()
// 生产者
for i := 1; i <= 5; i++ {
time.Sleep(50 * time.Millisecond)
fmt.Printf("生产: %d\n", i)
q.Push(i)
}
time.Sleep(600 * time.Millisecond)
}Cond.Wait() 会释放锁并阻塞,被 Signal() 或 Broadcast() 唤醒后重新获取锁。始终在循环中检查条件,因为唤醒可能是虚假的。
atomic 包
sync/atomic 提供原子操作,无需加锁即可安全地操作基本类型。
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var counter int64
var wg sync.WaitGroup
// 原子递增
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
atomic.AddInt64(&counter, 1)
}()
}
wg.Wait()
fmt.Printf("原子计数: %d\n", atomic.LoadInt64(&counter)) // 1000
// 原子存储和加载
var status int64
atomic.StoreInt64(&status, 1)
fmt.Printf("状态: %d\n", atomic.LoadInt64(&status))
// CompareAndSwap(CAS)
var enabled int64 = 0
swapped := atomic.CompareAndSwapInt64(&enabled, 0, 1)
fmt.Printf("CAS 成功: %v, 新值: %d\n", swapped, enabled)
// CAS 失败的情况
swapped2 := atomic.CompareAndSwapInt64(&enabled, 0, 2) // 当前值是 1,不是 0
fmt.Printf("CAS 失败: %v, 值不变: %d\n", swapped2, enabled)
// 使用 atomic.Value 存储任意类型
var config atomic.Value
config.Store(map[string]string{"env": "production"})
cfg := config.Load().(map[string]string)
fmt.Printf("配置: %v\n", cfg)
}原子操作比 Mutex 更轻量,适合简单的计数器和标志位。atomic.Value 可以原子地存储和加载任意类型的值。
同步工具对比
| 工具 | 适用场景 | 特点 |
|---|---|---|
WaitGroup |
等待多个 goroutine 完成 | 简单计数,不保护数据 |
Mutex |
保护共享资源的读写 | 互斥,任何时刻只有一个 goroutine 持有锁 |
RWMutex |
读多写少的共享数据 | 多读并发,写独占 |
Once |
单例初始化 | 保证只执行一次 |
Map |
并发 map(特定场景) | 无需加锁,适合缓存 |
Cond |
等待条件成立 | 配合锁使用,支持 Signal/Broadcast |
atomic |
简单的计数器/标志位 | 无锁操作,性能最高 |
总结
sync 包提供了 Go 并发编程所需的核心同步工具。WaitGroup 等待一组 goroutine 完成;Mutex 和 RWMutex 保护共享数据,后者在读多写少时更高效;Once 保证初始化只执行一次;sync.Map 为特定场景提供无锁并发 map;Cond 实现条件等待;atomic 包提供无锁的原子操作。选择合适的同步工具是编写高效并发程序的关键——简单计数用 atomic,保护复杂数据用 Mutex/RWMutex,等待完成用 WaitGroup。