关于通道的 happens-before 有哪些
关于 happens-before 的概念,维基百科上给出的定义:
In computer science, the happened-before relation (denoted: ->) is a relation between the result of two events, such that if one event should happen before another event, the result must reflect that, even if those events are in reality executed out of order (usually to optimize program flow).
简单来说就是如果事件 a 和事件 b 存在 happened-before 关系,即 a -> b,那么 a,b 完成后的结果一定要体现出这种关系。由于现代编译器、CPU 会做各种优化,包括编译器重排、内存重排等,在并发代码里,happened-before 限制就非常重要了。
关于 channel 的发送(send)、发送完成(send finished)、接收(receive)、接收完成(receive finished)的 happened-before 关系如下:
1)第 n 个 send 一定 happens-before 第 n 个 receive finished,无论是缓冲型还是非缓冲型的 channel。
2)对于容量为 m 的缓冲型 channel,第 n 个 receive 一定 happens-before 第 n+m 个 send finished。
3)对于非缓冲型的 channel,第 n 个 receive 一定 happens-before 第 n 个 send finished。
4)channel close 一定 happens-before receiver 得到通知。
第 1 条,从源码的角度看也是对的, send 不一定是 happens-before receive,因为有时候先 receive,然后 goroutine 被挂起,之后被 sender 唤醒,send happened after receive。但不管怎样,要想完成接收,一定是要先有发送。
第 2 条,缓冲型的 channel,当第 n+m 个 send 发生后,有下面两种情况:
若第 n 个 receive 没发生。这时,channel 被填满了,send 就会被阻塞。那当第 n 个 receive 发生时,sender goroutine 会被唤醒,之后再继续发送过程。这样,第 n 个 receive 一定 happensbefore 第 n+m 个 send finished。
若第 n 个 receive 已经发生过了,直接就符合了要求。
第 3 条,比较好理解。如果第 n 个 send 被阻塞,sender goroutine 挂起,第 n 个 receive 这时到来,先于第 n 个 send finished。如果第 n 个 send 未被阻塞,说明第 n 个 receive 早就在那等着了,它不仅可以实现 happens-before send finished,它还可以实现 happens-before send。
第 4 条, 回忆一下源码, 先设置完 closed = 1, 再唤醒等待的 receiver, 并将零值复制给 receiver。
关于 happens-before,这里再看一个例子:
package main
import "fmt"
var done = make(chan bool)
var msg string
func aGoroutine() {
msg = "hello world"
done <- true
}
func main() {
go aGoroutine()
<-done
fmt.Println(msg)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
上述代码先定义了一个 done channel 和一个待打印的字符串。 在 main 函数里,启动一个 goroutine,等待从 done 里接收到一个值后,执行打印 msg 的操作。如果 main 函数中没有 <-done 这行代码,打印出来的 msg 为空,因为 aGoroutine 来不及被调度,还来不及给 msg 赋值,主程序就会退出。而在 Go 语言里,主协程退出时不会等待其他协程。
加了 <-done 这行代码后,就会阻塞在此。等 aGoroutine 里向 done 发送了一个值之后,才会被唤醒,继续执行打印 msg 的操作。而在这之前,msg 已经被赋值过了,所以会打印出 “hello, world”。
这里依赖的 happens-before 就是前面讲的第一条。第一个 send 一定 happens-before 第一个 receive finished,即 done <- true 先于 <-done 发生,这意味着 main 函数里执行完 <-done 后接着 执行 println (msg) 这一行代码时,msg 已经被赋过值了,所以会打印出想要的结果。
进一步利用前面提到的第 3 条 happens-before 规则,修改一下代码:
var done = make(chan bool)
var msg string
func aGoroutine() {
msg = "hello, world"
<-done
}
func main() {
go aGoroutine()
done <- true
println(msg)
}
2
3
4
5
6
7
8
9
10
11
12
同样可以得到相同的结果, 为什么?根据第三条规则, 对于非缓冲型的 channel, 第一个 receive 一定 happens-before 第一个 send finished。也就是说,在 done <- true 完成之前,<-done 就已经发生了,也就意味着 msg 已经被赋上值了,最终也会打印出 hello, world。