Skip to content

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
}()
<-done

6.2.2 Channel 规则 ​

go
// 规则:channel 发送 happens before 接收完成
ch := make(chan int)

go func() {
    x = 1      // happens before
    ch <- 1    // 发送
}()

<-ch           // 接收完成
println(x)     // 保证看到 x=1

6.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 +0x4b

6.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
    └── 竞态检测

下一步 ​

  1. 阅读开源项目源码:Kubernetes、etcd、Docker
  2. 实践项目:用学到的模式构建自己的项目
  3. 性能调优:学习 pprof、trace 等工具
  4. 深入理解:阅读 Go runtime 源码