一、Happens-Before 原则
1.1 什么是 Happens-Before
Happens-Before 是 Go 内存模型的核心概念,定义了操作之间的可见性顺序。如果操作 A "happens-before" 操作 B,那么 A 的结果对 B 可见。
var x int
func main() {
x = 1 // 操作 A
fmt.Println(x) // 操作 B - 能看到 x=1
}
1.2 Go 中的 Happens-Before 规则
1.2.1 单线程规则:在单个 goroutine 中,操作按程序顺序发生
func singleGoroutine() {
a := 1 // 1
b := a + 1 // 2 - 能看到 a=1
c := b * 2 // 3 - 能看到 b=2
}
1.2.2 Channel 规则:Channel 操作创建 happens-before 关系
func channelHB() {
var c = make(chan int, 1)
var data string
// Goroutine 1
go func() {
data = "Codee君" // 1
c <- 1 // 2 - 发送
}()
<-c // 3 - 接收 happens-after 发送
fmt.Println(data) // 4 - 保证能看到 "Codee君"
}
1.2.3 Mutex/RWMutex 规则:互斥锁解锁 happens-before 后续加锁
func mutexHB() {
var mu sync.Mutex
var data int
// Goroutine 1
go func() {
mu.Lock() // 1
data = 42 // 2
mu.Unlock() // 3 - 解锁
}()
mu.Lock() // 4 - 发生在解锁之后
fmt.Println(data) // 5 - 保证能看到 42
mu.Unlock()
}
1.2.4 Once 规则:sync.Once.Do() 调用 happens-after 所有之前对 Do() 的调用返回
func onceHB() {
var once sync.Once
var data string
// setup 只会执行一次
setup := func() {
data = "initialized"
}
// 多个 goroutine 同时调用
for i := 0; i < 10; i++ {
go func() {
once.Do(setup) // 1 - 所有调用都会看到 setup 的结果
fmt.Println(data)
}()
}
}
1.2.5 WaitGroup 规则:Wait() 返回 happens-after 所有 Add() 调用
func waitGroupHB() {
var wg sync.WaitGroup
var results []int
for i := 0; i < 5; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
results = append(results, idx*2)
}(i)
}
wg.Wait() // 等待所有 goroutine 完成
fmt.Println(results) // 能看到所有结果
}
二、数据竞争(Data Race)
2.1 什么是数据竞争
当多个 goroutine 同时访问同一内存位置,且至少有一个是写操作,且没有适当的同步时,就发生了数据竞争。
// 有数据竞争的代码
func dataRace() {
var counter int
for i := 0; i < 1000; i++ {
go func() {
counter++ // 并发读写,有数据竞争!
}()
}
time.Sleep(time.Second)
fmt.Println(counter) // 结果不确定
}
2.2 检测数据竞争
使用 `-race` 标志检测数据竞争:
go run -race main.go
go test -race ./...
2.3 避免数据竞争的方法
2.3.1 使用互斥锁
func safeWithMutex() {
var (
mu sync.Mutex
counter int
)
for i := 0; i < 1000; i++ {
go func() {
mu.Lock()
counter++
mu.Unlock()
}()
}
time.Sleep(time.Second)
mu.Lock()
fmt.Println(counter) // 1000
mu.Unlock()
}
2.3.2 使用原子操作
import "sync/atomic"
func safeWithAtomic() {
var counter int64
for i := 0; i < 1000; i++ {
go func() {
atomic.AddInt64(&counter, 1)
}()
}
time.Sleep(time.Second)
fmt.Println(atomic.LoadInt64(&counter)) // 1000
}
2.3.3 使用 Channel
func safeWithChannel() {
var counter int
done := make(chan bool)
for i := 0; i < 1000; i++ {
go func() {
// 通过 channel 顺序处理
done <- true
}()
}
for i := 0; i < 1000; i++ {
<-done
counter++
}
fmt.Println(counter) // 1000
}
2.3.4 使用 sync.Map(适合读多写少)
func safeWithSyncMap() {
var m sync.Map
var wg sync.WaitGroup
// 并发写入
for i := 0; i < 100; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
m.Store(idx, idx*2)
}(i)
}
wg.Wait()
// 读取
m.Range(func(key, value interface{}) bool {
fmt.Printf("%v: %v\n", key, value)
return true
})
}
三、内存屏障(Memory Barrier)
3.1 什么是内存屏障
内存屏障是 CPU 指令,用于确保特定的内存操作顺序。Go 编译器会在需要时自动插入内存屏障。
3.2 Go 中的内存屏障
在以下情况下,Go 会插入内存屏障:
package main
import (
"sync"
"sync/atomic"
)
func memoryBarrierExamples() {
// 1. 原子操作自动包含内存屏障
var x int32
atomic.StoreInt32(&x, 1) // 存储屏障
val := atomic.LoadInt32(&x) // 加载屏障
// 2. Channel 操作包含内存屏障
ch := make(chan int, 1)
ch <- 1 // 发送屏障
<-ch // 接收屏障
// 3. 互斥锁操作包含内存屏障
var mu sync.Mutex
mu.Lock() // 获取屏障
mu.Unlock() // 释放屏障
// 4. Once 包含内存屏障
var once sync.Once
once.Do(func() {}) // 屏障确保初始化对其他 goroutine 可见
}
3.3 手动内存屏障(通常不需要)
import (
"runtime"
"sync/atomic"
)
func manualMemoryBarrier() {
var data int32
var ready int32
// Goroutine 1 - 写数据
go func() {
data = 42
atomic.StoreInt32(&ready, 1) // 存储屏障
}()
// Goroutine 2 - 读数据
go func() {
for atomic.LoadInt32(&ready) == 0 { // 加载屏障
runtime.Gosched() // 让出 CPU
}
fmt.Println(data) // 保证看到 42
}()
}
四、实际示例
4.1 双重检查锁定模式
type Singleton struct {
value string
}
var (
instance *Singleton
once sync.Once
mu sync.Mutex
initialized uint32
)
// 正确实现 1: 使用 sync.Once
func GetInstanceOnce() *Singleton {
once.Do(func() {
instance = &Singleton{value: "singleton"}
})
return instance
}
// 正确实现 2: 使用原子操作
func GetInstanceAtomic() *Singleton {
if atomic.LoadUint32(&initialized) == 1 {
return instance
}
mu.Lock()
defer mu.Unlock()
if initialized == 0 {
instance = &Singleton{value: "singleton"}
atomic.StoreUint32(&initialized, 1) // 内存屏障
}
return instance
}
4.2 发布-订阅模式
type Publisher struct {
subscribers []chan string
mu sync.RWMutex
}
func (p *Publisher) Subscribe() <-chan string {
ch := make(chan string, 1)
p.mu.Lock()
p.subscribers = append(p.subscribers, ch)
p.mu.Unlock()
return ch
}
func (p *Publisher) Publish(msg string) {
p.mu.RLock()
defer p.mu.RUnlock()
for _, ch := range p.subscribers {
select {
case ch <- msg:
default: // 避免阻塞
}
}
}
4.3 线程安全的缓存
type Cache struct {
data map[string]interface{}
mu sync.RWMutex
}
func (c *Cache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
val, ok := c.data[key]
return val, ok
}
func (c *Cache) Set(key string, value interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}
五、最佳实践
5.1 同步原则
-
不要通过共享内存来通信,而要通过通信来共享内存
-
将数据限制在单个 goroutine 中,通过 channel 传递
-
使用更高级的同步原语(sync.Once, sync.WaitGroup, sync.Map)
5.2 性能考虑
func performanceTips() {
// 1. 读多写少时使用 RWMutex
var mu sync.RWMutex
var cache map[string]string
// 读操作(可以并发)
func read(key string) string {
mu.RLock()
defer mu.RUnlock()
return cache[key]
}
// 写操作(互斥)
func write(key, value string) {
mu.Lock()
defer mu.Unlock()
cache[key] = value
}
// 2. 考虑使用 atomic.Value 存储不变的数据
var config atomic.Value
config.Store(map[string]string{"key": "value"})
// 3. 使用 sync.Pool 减少内存分配
pool := &sync.Pool{
New: func() interface{} {
return make([]byte, 1024)
},
}
buf := pool.Get().([]byte)
// 使用 buf...
pool.Put(buf)
}
5.3 调试技巧
func debugConcurrentIssues() {
// 1. 使用 -race 检测
// go test -race ./...
// 2. 使用 pprof 分析
// import _ "net/http/pprof"
// go func() {
// http.ListenAndServe("localhost:6060", nil)
// }()
// 3. 记录 goroutine ID(调试用)
func getGoroutineID() uint64 {
b := make([]byte, 64)
b = b[:runtime.Stack(b, false)]
// 解析堆栈获取 goroutine ID
return 0
}
// 4. 使用 context 控制 goroutine 生命周期
// ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
// defer cancel()
}
内存模型就像人生的因果律——你今天的学习(写操作)通过 happens-before 关系决定了明天的能力(读操作),而数据竞争就是那些缺乏纪律的念头在同时争夺你的注意力。
*源码地址*
1、公众号“Codee君”回复“每日一Go”获取源码
如果您喜欢这篇文章,请您(点赞、分享、亮爱心),万分感谢!