第 11 章:sync 包与并发安全
jerry北京市2026年5月9日Go 20 次阅读 约 12 分钟

掌握互斥锁、读写锁、WaitGroup、Once、sync.Map 和原子操作,编写并发安全的代码。
11.1 数据竞争
多个 goroutine 同时读写共享变量会导致数据竞争:
// 有数据竞争的代码
counter := 0
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter++ // 数据竞争!
}()
}
wg.Wait()
fmt.Println(counter) // 结果不确定,可能小于 1000
检测数据竞争:
go run -race main.go
go test -race ./...
11.2 Mutex(互斥锁)
var (
counter int
mu sync.Mutex
)
func increment() {
mu.Lock()
defer mu.Unlock()
counter++
}
// 使用
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
wg.Wait()
fmt.Println(counter) // 1000(确定的结果)
封装到结构体中
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
11.3 RWMutex(读写锁)
读多写少的场景,RWMutex 比 Mutex 性能更好:
type SafeMap struct {
mu sync.RWMutex
data map[string]string
}
func (m *SafeMap) Get(key string) (string, bool) {
m.mu.RLock() // 读锁:多个读可以并发
defer m.mu.RUnlock()
v, ok := m.data[key]
return v, ok
}
func (m *SafeMap) Set(key, value string) {
m.mu.Lock() // 写锁:独占
defer m.mu.Unlock()
m.data[key] = value
}
| 操作 | Mutex | RWMutex |
|---|---|---|
| 读-读 | 互斥 | 并发 |
| 读-写 | 互斥 | 互斥 |
| 写-写 | 互斥 | 互斥 |
11.4 WaitGroup
等待一组 goroutine 完成:
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("任务 %d 完成\n", id)
}(i)
}
wg.Wait()
fmt.Println("全部完成")
注意事项:
Add必须在 goroutine 启动前调用Done等价于Add(-1)Wait阻塞直到计数器归零- 计数器不能为负数(会 panic)
11.5 Once
确保某个操作只执行一次(典型场景:单例初始化):
var (
instance *Database
once sync.Once
)
func GetDB() *Database {
once.Do(func() {
fmt.Println("初始化数据库连接...")
instance = &Database{
// 初始化配置
}
})
return instance
}
// 无论调用多少次,初始化只执行一次
db1 := GetDB()
db2 := GetDB() // 直接返回已初始化的实例
11.6 sync.Map
并发安全的 map,适用于读多写少的场景:
var m sync.Map
// 存储
m.Store("key1", "value1")
m.Store("key2", 42)
// 读取
v, ok := m.Load("key1")
if ok {
fmt.Println(v) // value1
}
// 读取或存储(如果不存在则存储)
actual, loaded := m.LoadOrStore("key3", "default")
fmt.Println(actual, loaded) // default, false
// 删除
m.Delete("key1")
// 遍历
m.Range(func(key, value interface{}) bool {
fmt.Printf("%v: %v\n", key, value)
return true // 返回 false 停止遍历
})
sync.Map vs 加锁的 map:
- sync.Map 适合:读多写少、key 相对稳定
- 加锁 map 适合:读写均衡、需要批量操作
11.7 原子操作(atomic)
比锁更轻量的并发安全操作:
import "sync/atomic"
var counter int64
// 原子加
atomic.AddInt64(&counter, 1)
// 原子读
v := atomic.LoadInt64(&counter)
// 原子写
atomic.StoreInt64(&counter, 100)
// CAS(Compare And Swap)
swapped := atomic.CompareAndSwapInt64(&counter, 100, 200)
// 如果 counter == 100,则设为 200,返回 true
Go 1.19+ atomic.Int64
var counter atomic.Int64
counter.Add(1)
counter.Store(100)
v := counter.Load()
swapped := counter.CompareAndSwap(100, 200)
原子操作 vs 锁:
- 原子操作:适合简单的数值操作,性能更好
- 锁:适合复杂的临界区操作
11.8 sync.Cond
条件变量,用于 goroutine 之间的条件等待:
var (
mu sync.Mutex
cond = sync.NewCond(&mu)
ready bool
)
// 等待方
go func() {
cond.L.Lock()
for !ready {
cond.Wait() // 释放锁并等待通知
}
fmt.Println("收到通知,开始工作")
cond.L.Unlock()
}()
// 通知方
time.Sleep(time.Second)
cond.L.Lock()
ready = true
cond.Signal() // 唤醒一个等待者(Broadcast 唤醒所有)
cond.L.Unlock()
11.9 sync.Pool
对象池,用于复用临时对象,减少 GC 压力:
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func processRequest() {
buf := bufferPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
bufferPool.Put(buf)
}()
buf.WriteString("处理请求...")
fmt.Println(buf.String())
}
注意:Pool 中的对象可能在任何时候被 GC 回收,不要存储需要持久化的数据。
11.10 面试要点
-
Mutex 和 RWMutex 的区别?
- Mutex:读写都互斥
- RWMutex:读读并发,读写互斥,写写互斥
-
sync.Map 的适用场景?
- 读多写少,key 相对稳定
- 不适合频繁写入的场景
-
原子操作和锁的区别?
- 原子操作更轻量,适合简单数值操作
- 锁适合复杂临界区
-
sync.Pool 的作用?
- 复用临时对象,减少内存分配和 GC 压力
- 对象可能被 GC 回收,不保证持久性
-
如何检测数据竞争?
go run -race或go test -race
练习
- 使用 Mutex 实现一个并发安全的银行账户(存款、取款、查询余额)
- 使用 RWMutex 实现一个并发安全的缓存
- 使用 sync.Once 实现单例模式
- 使用 atomic 实现一个无锁计数器,与 Mutex 版本做性能对比
评论
登录 后发表评论
暂无评论