Go的内存模型定义了在并发环境下,对一个变量的读操作能够看到哪些写操作。理解这些规则,是写出正确并发程序的前提。
一、happens-before关系
Go的内存模型基于happens-before关系,定义了操作之间的顺序约束。如果操作A happens-before操作B,那么A的结果对B可见。
单线程内的顺序:在同一个goroutine中,代码的执行顺序就是happens-before顺序。x = 1; y = 2中,x = 1happens-beforey = 2。
初始化顺序:包级别的init函数在main之前执行,import的包初始化在依赖包之前完成。
goroutine创建:go func()调用happens-before新goroutine的执行。启动goroutine之前写入的变量,在goroutine内部可见。
goroutine销毁:goroutine的结束不保证happens-before任何事件。如果需要等待goroutine完成,必须使用同步机制。
二、同步原语的happens-before规则
Mutex:Unlockhappens-before后续的Lock。锁释放时写入的变量,在锁被重新获取后可见。
RWMutex:RUnlockhappens-before后续的RLock。Unlockhappens-before后续的RLock和Lock。
WaitGroup:Add在Wait之前调用,Donehappens-beforeWait返回。
Once:Do中的函数执行完成 happens-beforeDo返回。多个goroutine并发调用Do时,只有一个执行函数,其他等待函数完成。
三、Channel的同步语义
无缓冲channel的同步性:向无缓冲channel发送值 happens-before 从该channel接收值。这保证了发送方在发送之前写入的数据在接收方可见。
有缓冲channel:向缓冲channel发送值 happens-before 从该channel接收值(仅当缓冲区非空时)。如果缓冲区满,发送方阻塞,接收方先执行,此时的happens-before关系反转。
关闭channel:关闭channel happens-before 从该channel接收到零值。这保证了在关闭channel之前写入的数据,在接收方看到零值时已经可见。
四、原子操作的可见性
sync/atomic包提供的原子操作是低级的同步原语。原子操作本身保证原子性和可见性,但不提供happens-before关系(除非配合sync/atomic的内存顺序参数)。atomic操作的主要用途是构建无锁数据结构和计数器,不替代Mutex。
go
var counter int64 atomic.AddInt64(&counter, 1) v := atomic.LoadInt64(&counter)
原子操作的开销低于Mutex,但使用不当会导致难以调试的bug。
五、数据竞争与检测
数据竞争发生在两个或多个goroutine并发访问同一内存位置,且至少有一个是写操作,且没有同步机制。
bash
go build -race go test -race
-race标志启用数据竞争检测,在运行时记录内存访问,检测到竞争时输出报告。竞争检测有运行时开销(通常增加10倍执行时间),但在开发和测试阶段是必要的工具。
六、小结
Go的内存模型建立在happens-before关系之上。同步原语(Mutex、WaitGroup、Once)提供了显式的同步点,channel提供了通信同步,原子操作提供了底层控制。理解这些规则,是正确使用并发机制的前提。