关于内存同步的解释推荐看这个博客: 曹政谈内存重排
一、现象
思考如下代码片段的输出
var x, y int
go func() {
x = 1 // A1
fmt.Print("y:", y, " ") // A2
}()
go func() {
y = 1 // B1
fmt.Print("x:", x, " ") // B2
}()
因为两个协程是并行的,也没有加锁,会有数据竞争,所以程序的运行结果无法预测,但是我们可能希望输出是以下四种中的一种
y:0 x:1
x:0 y:1
x:1 y:1
y:1 x:1
然而,真实运行的有些情况会出乎我们预料
二、说明
- 在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。因为赋值和打印指向不同的变量,编译器可能会断定两条语句的顺序不会影响执行结果,并且会交换两个语句的执行顺序。
- 多核cpu机器,如果两个协程在不同的cpu核上执行,每个核有自己的缓存,这样一个协程的写入对于其他协程的Print,在主存同步之前就是不可见的。所以尽管协程A中一定需要观察到x=1执行成功后才会去读取y,但它没法确保自己观察到协程B对y的写入,所以A还可能会打印出y的一个旧版的值。
三、解决
在java中我们可以使用volatile来确保两个线程对同一个变量的可见性。
但是在go中可以用以下方式规避
- 将变量限制在一个协程中
- 使用锁或者channel
四、扩展
as-if-serial 语义
as-if-serial的意思是:不管指令怎么重排序,在单线程下执行结果不能被改变。不管是编译器级别还是处理器级别的重排序都必须遵循as-if-serial语义。
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序。但是as-if-serial规则允许对有控制依赖关系的指令做重排序,因为在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果,但是多线程下确有可能会改变结果。
- 数据依赖
int a = 1; // 1
int b = 2; // 2
int c = a + b; // 3
上述代码,a和b不存在依赖关系,所以1、2可以进行重排序;c依赖 a和b,所以3必须在1、2的后面执行。
- 控制依赖
public void use(boolean flag, int a, int b) {
if (flag) { // 1
int i = a * b; // 2
}
}
flag和i存在控制依赖关系。当指令重排序后,2这一步会将结果值写入重排序缓冲(Reorder Buffer,ROB)的硬件缓存中,当判断为true时,再把结果值写入变量i中。
happens-before 语义
Within a single goroutine, reads and writes must behave as if they executed in the order specified by the program. That is, compilers and processors may reorder the reads and writes executed within a single goroutine only when the reordering does not change the behavior within that goroutine as defined by the language specification. Because of this reordering, the execution order observed by one goroutine may differ from the order perceived by another. For example, if one goroutine executes a = 1; b = 2;, another might observe the updated value of b before the updated value of a.
To specify the requirements of reads and writes, we define happens before, a partial order on the execution of memory operations in a Go program. If event e1 happens before event e2, then we say that e2 happens after e1. Also, if e1 does not happen before e2 and does not happen after e2, then we say that e1 and e2 happen concurrently.
两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个 操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一 个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second)