一、Go内存模型:Happens-Before原则

Go内存模型定义了并发读写操作的可见性和排序保证。理解Happens-Before是写出正确并发代码的基础,也是诊断data race的根本依据。

1.1 Happens-Before的精确定义

// Happens-Before的定义:
// 在同一goroutine中,代码顺序即Happens-Before顺序
// 在不同goroutine中,必须通过同步事件建立Happens-Before关系

package main

import "fmt"

var a, b int

func worker() {
    a = 1       // S1
    b = 2       // S2
    fmt.Println(b) // S3
}

// 问题:S3能保证读到a=1吗?
// 答案:在同一goroutine中,S1 Happens-Before S2 Happens-Before S3
//       所以S3执行时,a=1和b=2都已生效

// ⚠️ 跨goroutine的陷阱:
var x int

func goroutineA() {
    x = 1        // G1
}
func goroutineB() {
    if x == 1 { // G2
        println("x is 1")
    }
}

// G2可能读到x=0(编译器和CPU可以重排序)
// 原因:G1和G2之间没有同步事件,不存在Happens-Before关系

// ✅ 正确做法:使用Channel或sync原语
var mu sync.Mutex
var x int

func goroutineA() {
    mu.Lock()
    x = 1
    mu.Unlock()
}
func goroutineB() {
    mu.Lock()
    if x == 1 {
        println("x is 1")
    }
    mu.Unlock()
}
// Mutex的Lock Happens-Before Unlock
// 解锁后,后续Lock能保证看到之前的所有写入

二、Goroutine调度器:GMP模型

2.1 GMP调度架构

// Go调度器的三级模型:GPM
//
// G (Goroutine): 轻量级执行单元
//   - 栈初始2KB,最大可扩至1GB(按需增长)
//   - 包含:PC、SP、栈指针、状态
//
// M (Machine/Thread): OS线程
//   - 直接对应OS线程,绑定一个P
//   - G在M上执行(最多GOMAXPROCS个M同时运行)
//
// P (Processor): 虚拟CPU
//   - 管理G的运行队列(local rq + global rq)
//   - 数量 = GOMAXPROCS(默认CPU核数)
//
//              OS
//         ┌────────────┐
//         │   CPU核心   │
//         └─────┬──────┘
//               │ syscall / hardware interrupt
//         ┌─────┴──────┐
//         │     M       │ ← 系统线程
//         │  (Machine)  │
//         └─────┬──────┘
//               │ 绑定
//         ┌─────┴──────┐
//         │     P       │ ← 虚拟CPU
//         │(Processor)  │
//         │  rq: [G1,G2,G3,G4]
//         └─────┬──────┘
//               │ 调度
//         ┌─────┴──────┐
//         │     G      │ ← Goroutine
//         │  Goroutine │
//         └────────────┘

2.2 调度时机与抢占式调度

// Goroutine调度时机(6种触发条件):
//
// ① 系统调用返回(Sysmon检测到P阻塞)
// M1绑定的P1被syscall阻塞 → M1脱离P1
// → 从空闲M队列取M2,绑定P1,继续调度
//
// ② GC完成后(STW → 重新调度)
// stopTheWorld() → GC → startTheWorld()
// → 重新均衡所有P的runnable队列
//
// ③ channel阻塞/解锁
// ch := make(chan int)
// go func() { val := <-ch }()  // goroutine阻塞在ch
// ch <- 42                      // 唤醒goroutine
//
// ④ sync.Mutex / sync.WaitGroup
// wg.Add(1)
// go func() { defer wg.Done(); work() }()
// wg.Wait()  // 阻塞直到计数器归零
//
// ⑤ 主动让出(runtime.Gosched)
// func worker() {
//     result := compute()
//     runtime.Gosched()  // 主动让出CPU,放回队列末尾
//     process(result)
// }
//
// ⑥ 时间片轮转(Go 1.14+抢占式调度)
// 每10ms,sysmon向运行中的G发送sigurg
// → G中断,调度器介入 → 选择下一个G执行
// ⚠️ 没有时间片轮转会导致"孤独goroutine"问题:
//    for { } 无限循环 → 其他goroutine永远无法调度

三、内存分配:TCMalloc与逃逸分析

3.1 三层内存分配器

// Go内存分配器基于TCMalloc(Thread-Caching Malloc)
// 三级缓存:mcache → mcentral → mheap
//
// ┌─────────────────────────────────────────────────────┐
// │                   mheap (全局堆)                    │
// │  span: 8KB~32MB的内存页管理                         │
// │  用于分配大对象(>32KB)和补充mcache                │
// └─────────────────────────────────────────────────────┘
//                          ↑ fill/alloc
// ┌─────────────────────────────────────────────────────┐
// │               mcentral (共享central)               │
// │  按sizeclass分span:每类有empty和nonempty列表       │
// │  竞争:所有P共享,需要加锁                           │
// └─────────────────────────────────────────────────────┘
//                          ↑ refill/alloc
// ┌─────────────────────────────────────────────────────┐
// │                  mcache (per-P缓存)               │
// │  每个P一个mcache,无锁(因为每个P独享)             │
// │  包含sizeclass对应的小对象分配                       │
// └─────────────────────────────────────────────────────┘

// sizeclass(67个级别,8B~32KB)
// class 1:  8B   → 每块8B
// class 2:  16B  → 每块16B
// class 3:  32B  → 每块32B
// ...
// class 67: 32KB → 每块32KB
// >32KB: 直接从mheap分配(large object allocation)

3.2 逃逸分析(Escape Analysis)

// 逃逸分析:决定变量分配在栈还是堆
// 查看逃逸分析结果:
// $ go build -gcflags '-m=2' main.go

// 逃逸案例:
func main() {
    // ❌ 错误理解:new返回指针,一定在堆上
    p := new(int)    // 编译器分析后,可能分配在栈上!
    *p = 42

    // 实际上:如果p不逃逸,分配在栈上(无GC开销)
    // 验证:
    // go build -gcflags '-m=2' → new(int) does not escape
}

// 强制逃逸的场景(一定在堆上):
func bad() []int {
    s := make([]int, 0, 10)  // slice的底层array可能逃逸
    return s                  // 返回slice,array不能栈上分配
}

// 正确的"池化"写法(减少GC压力):
func good() []int {
    s := make([]int, 0, 10) // 栈上临时
    process(s)              // 不返回,只在函数内用
    return s                // 栈上OK
}

// channel逃逸(共享内存跨goroutine,必须在堆上):
func producer(ch chan<- int) {
    ch <- 42  // 写入channel的值可能逃逸到堆
}

// 全局变量逃逸:
var global *int

func setGlobal() {
    // 编译器保守估计:指针可能跨作用域
    // global变量在堆上,ptr也逃逸到堆
    ptr := new(int)
    global = ptr
}

// 性能影响:栈分配(~2ns)vs 堆分配+GC(~100ns+)
// 差距:50倍!逃逸分析是Go性能优化的重要环节

四、内存屏障与并发安全

// Go的同步原语与内存屏障
//
// sync.Mutex 实现的Happens-Before:
// - Unlock() Happens-Before 后续Lock()
// - Unlock() Happens-Before 所有后续操作
//
// 示例:
var balance int
var mu sync.Mutex

func Deposit(amount int) {
    mu.Lock()
    balance += amount
    mu.Unlock()  // Unlock Happens-Before 后续读取
}

func Balance() int {
    mu.Lock()
    defer mu.Unlock()
    return balance
}

// sync.Once的Happens-Before:
// f()调用 Happens-Before 所有Done()返回
// → 保证了初始化代码的执行顺序
var once sync.Once
var config *Config

func GetConfig() *Config {
    once.Do(func() {
        config = loadConfig() // 只执行一次
    })
    return config
}

// atomic.Value的内存模型:
// 写入 Happen-Before 读取
var v atomic.Value
v.Store(data)        // 写入
go func() {
    r := v.Load()    // 读取时能看到Store的内容
}()

// ⚠️ atomic.Value不能存储指针的指针(指针本身是值的拷贝)