happens-before 内存模型
注:happens-before 是一个术语,并不仅仅是 Go 语言才有的。
一、概念
假设 A 和 B 表示一个多线程的程序执行的两个操作,如果 A happens-before B,那么 A 操作对内存的影响将对执行 B 的线程可见(执行 B 之前)。 内存模型描述的是 “在一个 groutine 中对变量进行读操作能够侦测到在其他 gorountine 中对该变量的写操作” 的条件。
二、误区
happens-before 并不是指时序关系,并不是说 A happens-before B 就表示操作 A 在操作 B 之前发生。比如:指令重排,第二条机器指令(例如对 B 赋值)完成时,对 A 的赋值还没有完成,对 A 的赋值其实并没有对 B 的赋值有影响。即便对 A 赋值的影响真的可见,对 B 的赋值的行为还是一样。所以,这并不能算是违背 happens-before 规则。
三、go 中 happen before 保证场景
单线程。在只有一个 goroutine 时,不存在与 w1 或者 r1 并发的写操作,一个读操作 r1 总是对最近的一个写操作 w1 可见。但是当有多个 goroutine 并发访问同一个变量时,就需要引入同步机制来建立 happen-before 条件来确保读操作 r1 对写操作 w1 可见。
注:在 go 内存模型中,将多个 goroutine 中都会用到的全局变量初始化为它的类型零值被视为一次写操作,另外当读取一个类型大小比机器字长大的变量的值时候表现为是对多个机器字的多次读取,这个行为是未知的,go 中使用sync/atomic包中的 Load 和 Store 操作可以解决这个问题。解决多 goroutine 下共享数据可见性问题的方法是在访问共享数据时候施加一定的同步措施,比如 sync 包下的锁或者通道。
Init 函数。如果包 P1 中导入了包 P2,则 P2 中的 init 函数 Happens Before 所有 P1 中的操作;main 函数 Happens After 所有的 init 函数。
Goroutine。Goroutine 的创建 Happens Before 所有此 Goroutine 中的操作;Goroutine 的销毁 Happens After 所有此Goroutine 中的操作。
Channel。
对一个元素的 send 操作 Happens Before 对应的 receive 操作
var c = make(chan int, 1) var a string func f() { a = "Hello world" c <- 1 } func main() { go f() res := <-c println(a) print(res) } // 输出 “Hello world 1” // 能够满足 “一个 channel 上的发送是在该 channel 的相应接收完成之前发生的”
对 channel 的 close 操作 Happens Before receive 端的收到关闭通知操作
var c = make(chan int, 1) var a string func f() { a = "Hello world" close(c) } func main() { go f() res := <-c println(a) print(res) } // 输出 “Hello world 0” // 能够满足 “channel 的关闭发生在接收之前,因为通道被关闭而返回一个对应类型的零值”
对于 Unbuffered Channel,对一个元素的 receive 操作 Happens Before 对应的 send 完成操作
var c = make(chan int) var a string func f() { a = "Hello world" <-c } func main() { go f() c <- 1 print(a) } // 正常输出 “Hello world” // 能够满足 “一个无缓冲 channel 的接收发生在往该 channel 的发送之前”。 // 如果我们把无缓冲改为 make(chan int, 1),也就是带缓冲的 channel,则无法保证正常的输出 “Hello world”
对于 Buffered Channel,假设 Channel 的 buffer 大小为 N ,那么对第 k 个元素的 receive 操作,Happens Before 第 k+N 个 send 完成操作。可以看出上一条 Unbuffered Channel 规则就是这条规则 N=0 时的特例。
// 这个程序启动多个 goroutine, // 但 goroutine 使用 channel 进行协调,以确保每次最多只有三个 goroutine 在运行。 var limit = make(chan int, 3) func main() { wg := sync.WaitGroup{} for i := 0; i < 5; i++ { wg.Add(1) go func() { limit <- 1 fmt.Println("send") <-limit fmt.Println("receive") wg.Done() }() } wg.Wait() } // 能够满足 “一个容量为 N 的 channel 上,第 k 次接收发生在该 channel 的第 k+N 次发送完成之前” // 简单讲,就是必须等第一个协程将已满通道中的值接收掉一个之后,第四个协程才能往通道里发送新值
Lock
Once
once.Do
中执行的操作(执行且仅执行一次),Happens Before 任何一个once.Do
调用的返回。
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: