每日一Go-37、Go 内存模型--Happens-Before / 数据竞争原理 / 屏障指令

0 阅读5分钟

一、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”获取源码

2、pan.baidu.com/s/1B6pgLWfS… 


如果您喜欢这篇文章,请您(点赞、分享、亮爱心),万分感谢!