6. 内存模型与竞态检测
理解 Go 内存模型和 Happens-Before 规则,避免竞态条件。
6.1 Go 内存模型
6.1.1 什么是内存模型?
Go 内存模型定义了在不同 goroutine 中,对同一变量的读写操作何时可见。
┌─────────────────────────────────────────────────────────────┐
│ Go 内存模型 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Goroutine 1 内存 Goroutine 2 │
│ ┌─────────┐ ┌───────┐ ┌─────────┐ │
│ │ x = 1 │ ────► │ x=1 │ ◄──── │ print │ │
│ │ │ │ │ │ (x) │ │
│ └─────────┘ └───────┘ └─────────┘ │
│ │
│ 问题:Goroutine 2 何时能看到 x=1? │
│ 答案:由内存模型和同步原语决定 │
│ │
└─────────────────────────────────────────────────────────────┘6.2 Happens-Before 规则
6.2.1 基本规则
go
// 规则 1: 同一 goroutine 内,代码顺序执行
func main() {
x = 1 // happens before
y = 2 // happens before
z = x + y
}
// 规则 2: goroutine 创建
go func() {
x = 1 // happens before goroutine 启动
}()
// 规则 3: goroutine 退出
done := make(chan struct{})
go func() {
x = 1
close(done) // happens before <-done
}()
<-done6.2.2 Channel 规则
go
// 规则:channel 发送 happens before 接收完成
ch := make(chan int)
go func() {
x = 1 // happens before
ch <- 1 // 发送
}()
<-ch // 接收完成
println(x) // 保证看到 x=16.3 竞态条件
6.3.1 什么是竞态条件?
当多个 goroutine 并发访问同一变量,且至少有一个是写操作时,就可能发生竞态条件。
go
// 错误示例:竞态条件
var count int
func increment() {
count++ // 不是原子操作!
}
// 正确示例:使用 Mutex
var (
mu sync.Mutex
count int
)
func increment() {
mu.Lock()
defer mu.Unlock()
count++
}
// 正确示例:使用原子操作
var count int64
func increment() {
atomic.AddInt64(&count, 1)
}6.3.2 常见竞态场景
go
// 场景 1: 检查后使用 (TOCTOU)
if _, ok := m[key]; ok {
m[key] = value // 竞态!
}
// 正确:使用 Mutex
mu.Lock()
if _, ok := m[key]; ok {
m[key] = value
}
mu.Unlock()
// 场景 2: 单例初始化
var instance *Singleton
func GetInstance() *Singleton {
if instance == nil { // 竞态!
instance = &Singleton{}
}
return instance
}
// 正确:使用 sync.Once
var (
instance *Singleton
once sync.Once
)
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{}
})
return instance
}6.4 竞态检测
6.4.1 使用 race detector
bash
# 编译时启用
go build -race main.go
# 运行时启用
go run -race main.go
# 测试时启用
go test -race ./...6.4.2 检测示例
go
package main
import (
"fmt"
"sync"
)
var count int
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
count++ // race detector 会报告这里
}()
}
wg.Wait()
fmt.Println(count)
}输出:
WARNING: DATA RACE
Read at 0x0000000000000000 by goroutine 7:
main.main.func1()
main.go:15 +0x3a
Previous write at 0x0000000000000000 by goroutine 6:
main.main.func1()
main.go:15 +0x4b6.5 原子操作
6.5.1 sync/atomic 包
go
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var count int64
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
atomic.AddInt64(&count, 1)
}()
}
wg.Wait()
fmt.Println(atomic.LoadInt64(&count))
}6.5.2 原子操作类型
go
// 整数原子操作
atomic.AddInt64(&val, 1) // 加
atomic.LoadInt64(&val) // 读
atomic.StoreInt64(&val, 100) // 写
atomic.SwapInt64(&val, 100) // 交换
atomic.CompareAndSwapInt64(...) // CAS
// 指针原子操作
var ptr unsafe.Pointer
atomic.StorePointer(&ptr, unsafe.Pointer(&value))
loaded := atomic.LoadPointer(&ptr)6.6 性能优化
6.6.1 减少锁竞争
go
// 错误:锁粒度过大
type Cache struct {
mu sync.Mutex
data map[string]string
}
func (c *Cache) Get(key string) string {
c.mu.Lock() // 读也加锁,性能差
defer c.mu.Unlock()
return c.data[key]
}
// 正确:使用 RWMutex
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) string {
c.mu.RLock() // 读锁,可并发
defer c.mu.RUnlock()
return c.data[key]
}6.6.2 使用 sync.Map
go
// 适用于读多写少的场景
var m sync.Map
// 存储
m.Store("key", "value")
// 读取
val, ok := m.Load("key")
// 读取或存储
val, loaded := m.LoadOrStore("key", "value")
// 删除
m.Delete("key")6.7 本章小结
┌─────────────────────────────────────────────────────────────┐
│ 本章总结 │
├─────────────────────────────────────────────────────────────┤
│ ✓ Go 内存模型定义变量可见性规则 │
│ │
│ ✓ Happens-Before 规则决定操作顺序 │
│ │
│ ✓ 竞态条件:多 goroutine 并发访问,至少一个写 │
│ │
│ ✓ 使用 -race 检测竞态条件 │
│ │
│ ✓ 原子操作:sync/atomic 包 │
│ │
│ ✓ 性能优化:减少锁竞争、使用 sync.Map │
└─────────────────────────────────────────────────────────────┘完整教程总结
恭喜完成 Go 并发编程深度解析教程!
知识体系
Go 并发编程
├── Goroutine (执行单元)
│ ├── GMP 模型
│ └── 调度器
├── Channel (通信机制)
│ ├── 管道模式
│ └── 扇入扇出
├── sync 包 (同步原语)
│ ├── Mutex/RWMutex
│ ├── WaitGroup
│ ├── Cond
│ └── Once
├── Context (控制)
│ ├── 取消信号
│ ├── 截止时间
│ └── 值传递
├── 并发模式
│ ├── Worker Pool
│ ├── 生产者 - 消费者
│ └── 发布 - 订阅
└── 内存模型
├── Happens-Before
└── 竞态检测下一步
- 阅读开源项目源码:Kubernetes、etcd、Docker
- 实践项目:用学到的模式构建自己的项目
- 性能调优:学习 pprof、trace 等工具
- 深入理解:阅读 Go runtime 源码