happens_before以及golang的运用
Happens-before 是一个内存模型中的核心概念,它定义了在并发程序中,一个操作(如写入变量)的结果保证对另一个操作(如读取该变量)可见的规则。
它不是关于“真实物理时间”的先后,而是一种关于内存操作顺序和可见性的逻辑承诺。如果事件 A happens-before
事件 B,那么 A 操作对内存产生的所有影响,都必须在 B 操作开始之前,对 B 完全可见。
它解决了并发编程中最棘手的两个问题:指令重排和内存可见性。
- 对抗混乱的“幕后优化”:为了追求极致性能,编译器和 CPU 会对你的代码进行“优化”,比如打乱指令的执行顺序 (Instruction Reordering)。在单线程中这通常没问题,但在并发环境下,这种重排可能导致灾难性的后果。
- 确保数据同步:在现代多核 CPU 架构中,每个核心都有自己的高速缓存 (Cache)。一个核心对数据的修改不会立即同步到主内存或其他核心的缓存中。这就导致一个 goroutine 修改了数据,另一个 goroutine 可能根本“看不见”这个修改。
1. 介绍
1.1 happens-before 保证
如果A happens-before B,那么A操作对内存的影响 将对执行B的线程(且执行B之前)可见。
- 如果操作A和B在相同的线程中执行,并且A操作的声明在B之前,那么A happens-before B。
- happens-before关系都是可传递的:如果A happens-before B,同时B happens-before C,那么A happens-before C。当这些关系发生在不同的线程中,传递性将变得非常有用。
1.2 和时间无关
1 | var a string |
避坑:done
的写入和 a
的写入对 main
goroutine 的可见性没有保证。即使 done
变成了 true
,a
的赋值也可能还没“同步”过来。必须使用 Channel 或 Mutex 来建立 happens-before
关系。
在(1)和(2)之间存在happens-before 关系,同时在(3)和(4)之间也存在happens-before关系。并不意味着在(2)和(3)之间存在happens-before 关系!
2. golang 的 happens-before
2.1 场景保证
- 包的初始化 (Initialization)
main
函数的执行happens-after
所有init
函数执行完毕。init
函数的执行顺序遵循包的导入顺序。
- Goroutine 的创建与销毁
- 启动一个新 goroutine 的
go
语句happens-before
该 goroutine 内代码的执行。 - 一个 goroutine 的退出(无论是正常返回还是
panic
)不保证happens-before
程序中的任何其他事件。这就是为什么需要sync.WaitGroup
来等待 goroutine 完成。
- 启动一个新 goroutine 的
- Channel 通信 (最核心的 Go 特色)
- 对一个 Channel 的发送操作
happens-before
从该 Channel 完成的接收操作。 - 关闭一个 Channel 的操作
happens-before
从该 Channel 因关闭而收到零值的接收操作。 - 对于无缓冲 Channel,发送操作
happens-before
接收操作完成。 - 对于有缓冲 Channel(容量为 C),第
k
次接收happens-before
第k+C
次发送完成
- 对一个 Channel 的发送操作
- 锁 (
sync.Mutex
和sync.RWMutex
)- 对一个
Mutex
的第n
次Unlock()
调用happens-before
第n+1
次Lock()
调用返回。
- 对一个
sync.Once
once.Do(f)
中函数f
的返回happens-before
任何后续的once.Do(f)
调用返回。
sync.WaitGroup
wg.Add(n)
调用和n
次wg.Done()
调用happens-before
wg.Wait()
的返回。
2.2 有无缓冲 channel 区别
无缓冲 Channel (Unbuffered Channel):其核心目的是同步 (Synchronization)。它强制发送方和接收方“握手”。
有缓冲 Channel (Buffered Channel):其核心目的是解耦 (Decoupling)。它在发送方和接收方之间提供一个缓冲区,以减少等待。
这个核心目的的不同,直接导致了它们 happens-before
保证的强弱差异。
类型 | Happens-before 保证 | 行为描述 |
---|---|---|
无缓冲 Channel | 强同步保证 | 对 Channel 的发送操作 happens-before 从该 Channel 完成的接收操作。因为发送方会一直阻塞,直到接收方准备好并取走数据。 |
有缓冲 Channel | 弱同步保证 | 对 Channel 的发送操作 happens-before 从该 Channel 完成的接收操作。这个规则依然成立,但发送操作完成不等于发送方阻塞。只要缓冲区未满,发送方会立刻完成并继续执行。 |
关键点在于“发送操作完成”这个词的含义。
对于无缓冲 Channel,“发送操作完成”意味着接收方已经取走了数据。
对于有缓冲 Channel,“发送操作完成”意味着数据已经成功放入了缓冲区,发送方可以继续干别的事了,它根本不知道接收方什么时候会来取。
生产者只需把货物放进仓库就可以离开,消费者有空了自己去仓库取。这解耦了二者的执行节奏,提升了整体吞吐量。
happens-before
保证的是你放入仓库的那个货物本身是完整的,但它不保证你放下货物的那一刻,消费者就已经站在仓库门口了。
无缓冲 Channel → “当面递送挂号信” (Relay Race Baton Pass)
有缓冲 Channel → “投递到小区的信箱” (Mailbox)
一个带缓存的channel,则print(a)打印的结果不能够保证是”hello world”。
1 | var c = make(chan int, 1) |
因为这里不再有任何同步保证,使得(2) happens-before (3)。
3. 提问问题
3.1 注意事项
永远不要依赖 time.Sleep()
或者忙等待(for {}
)来进行 goroutine 间的同步。这些方法完全不提供任何 happens-before
保证,是典型的“侥幸编程”。