happen-befores原则 使用

happens-before 在一个 goroutine 内部,程序的执行顺序和它们的代码指定的顺序是一样的,即使编译器 或者 CPU 重排了读写顺序,从行为上来看,也和代码指定的顺序一样。

我们来看一个例子。在下面的代码中,即使编译器或者 CPU 对 a、b、c 的初始化进行了 重排,但是打印结果依然能保证是 1、2、3,而不会出现 1、0、0 或 1、0、1 等情况。

1
2
3
4
5
6
7
8
func foo() {
    var a = 1
    var b = 2
    var c = 3
    println(a)
    println(b)
    println(c)
}

在 Go 语言中,对变量进行零值的初始化就是一个写操作。

  1. 如果对超过机器 word(64bit、32bit 或者其它)大小的值进行读写,那么,就可以看 作是对拆成 word 大小的几个读写无序进行。
  2. Go 并不提供直接的 CPU 屏障(CPU fence)来提示编译器或者 CPU 保证顺序性, 是使用不同架构的内存屏障指令来实现统一的并发原语。

包级别的变量在同一个文件中是按照声明顺序逐个初始化的,除非初始化它的时候依赖其 它的变量。同一个包下的多个文件,会按照文件名的排列顺序进行初始化。这个顺序被定 义在 Go 语言规范中,而不是 Go 的内存模型规范中。你可以看看下面的例子中各个变量 的值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var (
    a = c + b // == 9
    b = f() // == 4
    c = f() // == 5
    d = 3 // == 5 全部初始化完成后
)
func f() int {
	d++
	return d
}

Go 采用的是依赖分析技术。不过,依赖分析技术保证 的顺序只是针对同一包下的变量,而且,只有引用关系是本包变量、函数和非接口的方 法,才能保证它们的顺序性。

goroutine 首先,我们需要明确一个规则:启动 goroutine 的 go 语句的执行,一定 happens before 此 goroutine 内的代码执行。 根据这个规则,我们就可以知道,如果 go 语句传入的参数是一个函数执行的结果,那 么,这个函数一定先于 goroutine 内部的代码被执行。

我们来看一个例子。在下面的代码中,第 8 行 a 的赋值和第 9 行的 go 语句是在同一个 goroutine 中执行的,所以,在主 goroutine 看来,第 8 行肯定 happens before 第 9 行,又由于刚才的保证,第 9 行子 goroutine 的启动 happens before 第 4 行的变量输 出,那么,我们就可以推断出,第 8 行 happens before 第 4 行。也就是说,在第 4 行打 印 a 的值的时候,肯定会打印出“hello world”。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var a string
func f() {
	print(a)
}


func hello() {
	a = "hello, world"
	go f()
}

谨慎地使用这些保证,能够让你的程序按照设想的 happens-before 关系执行,但是不要 以为完全理解这些概念和保证,就可以随意地制造所谓的各种技巧,否则就很容易掉 进“坑”里,而且会给代码埋下了很多的“定时炸弹”。