Go并发系列:7内存模型-7.2 happen-before原则

177 阅读3分钟

7.2 Happen-Before原则

在并发编程中,理解操作之间的执行顺序和可见性是至关重要的。Go 内存模型通过 Happen-Before 原则来定义并发操作之间的顺序关系。Happen-Before 原则确保了在不同 Goroutine 之间,某些操作的结果对其他操作是可见的。

7.2.1 什么是 Happen-Before 原则

Happen-Before 原则定义了在多线程或多 Goroutine 环境中,操作之间的顺序关系。如果一个操作 A "happens-before" 另一个操作 B,则 A 的结果在 B 开始之前对 B 可见。换句话说,A 操作完成之后,B 操作才能开始。

Happen-Before 原则的关键点在于建立一个清晰的顺序,使得并发程序中的操作在不同 Goroutine 之间具有一致性和可预测性。

7.2.2 Happen-Before 关系的建立

在 Go 内存模型中,有几种常见的方式来建立 Happen-Before 关系:

  1. 程序顺序规则:在单个 Goroutine 内,按顺序执行的操作之间天然存在 Happen-Before 关系。即 A; B 中,A happens-before B。

    func example() {
        a := 1
        b := 2 // a happens-before b
    }
    
  2. 通道发送和接收:对同一个通道的发送操作 send happens-before 对同一个通道的接收操作 recv,只要 recv 成功接收到 send 发送的数据。

    ch := make(chan int)
    go func() {
        ch <- 1 // send happens-before receive
    }()
    val := <-ch
    
  3. 互斥锁:对 Mutex 的解锁操作 Unlock happens-before 同一个 Mutex 的加锁操作 Lock

    var mu sync.Mutex
    var count int
    
    go func() {
        mu.Lock()
        count++
        mu.Unlock() // Unlock happens-before the next Lock
    }()
    
    mu.Lock()
    fmt.Println(count) // Lock happens-after the previous Unlock
    mu.Unlock()
    
  4. WaitGroup 操作:对 WaitGroupAdd 操作 happens-before Wait 操作。

    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
        defer wg.Done()
        fmt.Println("Goroutine finished") // Add happens-before Wait
    }()
    wg.Wait()
    fmt.Println("All done")
    
  5. 一次性操作:对 sync.OnceDo 方法的调用 happens-before 其他对该 Once 对象的 Do 方法的调用。

    var once sync.Once
    once.Do(func() {
        fmt.Println("Only once") // First Do happens-before subsequent Do calls
    })
    
  6. 原子操作:所有原子操作按顺序执行,确保原子操作之间存在 Happen-Before 关系。

    var x int32
    atomic.StoreInt32(&x, 1) // Store happens-before Load
    val := atomic.LoadInt32(&x)
    

7.2.3 Happen-Before 原则的应用

理解并应用 Happen-Before 原则,可以确保并发程序的正确性和安全性,避免数据竞争和不确定行为。以下是一些应用 Happen-Before 原则的示例:

示例 1:通过 Channel 通信建立顺序

package main

import (
    "fmt"
)

func main() {
    ch := make(chan int)

    go func() {
        ch <- 1 // 发送操作
    }()

    value := <-ch // 接收操作
    fmt.Println(value) // 输出 1,确保接收到的数据是发送的数据
}

示例 2:通过 Mutex 保护共享资源

package main

import (
    "fmt"
    "sync"
)

var (
    count int
    mu    sync.Mutex
)

func increment(wg *sync.WaitGroup) {
    defer wg.Done()
    mu.Lock()
    defer mu.Unlock()
    count++
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go increment(&wg)
    }
    wg.Wait()
    fmt.Println("Final count:", count) // 确保所有操作完成后输出最终计数值
}

示例 3:使用 WaitGroup 等待 Goroutine 完成

package main

import (
    "fmt"
    "sync"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Printf("Worker %d started\n", id)
    // 模拟工作
    fmt.Printf("Worker %d finished\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go worker(i, &wg)
    }

    wg.Wait() // 等待所有 Goroutine 完成
    fmt.Println("All workers done")
}

结论

Happen-Before 原则是理解和构建并发程序的基础。通过掌握和应用这些原则,可以确保并发操作的顺序性和可见性,从而避免数据竞争和不确定行为。在接下来的章节中,我们将继续探讨 Go 并发编程的高级特性和最佳实践。