Go 官网有一段代码例子:
var c = make(chan int)
var a string
func f() {
a = "hello, world"
<-c
}
func main() {
go f()
c <- 0
print(a)
}
官网说使用了 channel 后,这段代码可以确保能正常打印出"hello, world",原因是什么?
这是我的理解(有可能不对,欢迎指正): 假设 [ f 协程] 运行在 cpu0 核心上, [ main 协程] 运行在 cpu1 核心上, [ f 协程] 修改完 a 变量后,由于不同 cpu 核心之间存在缓存一致性问题,这个修改对于 [ main 协程] 来说有可能是不可见的,也就是 [ main 协程] 有可能会打印出一个空字符串
那么,channel 在这段代码里发挥的作用是什么,它是怎么确保 [ main 协程] 可以正常打印出"hello, world"的呢?
101
Evrins 347 天前 1
@rockyliang flag = false 的 写操作被 golang 优化了, 因为后面没有读操作了, 后面如果再打印 flag 的话就预期一样了
```go func Test_goroutine_value(t *testing.T) { flag := true // 协程 A go func() { fmt.Printf("Goroutine start\n") for flag { fmt.Printf("Goroutine flag: %v\n", flag) time.Sleep(time.Second * 1) continue } fmt.Printf("Goroutine finish\n") }() for { flag = false fmt.Printf("flag: %v\n", flag) time.Sleep(time.Second) // 很重要, 不然会爆炸 continue } } ``` 另外一方面 golang 协程的范式应该通过 channel 在不同的 goroutine 之间传递数据, 而不是共享内存 `Do not communicate by sharing memory; instead, share memory by communicating.` |
102
nuk 347 天前
这个其实是锁的基础,基于执行时间先后的同步,默认情况下处理器是需要支持的,当然也有不支持的情况。
假如有 A ,B ,C 三个指令,如果 ABC 按照时间顺序依次执行,依然会产生不同的结果,那么锁就失去了意义。 因为乱序执行或者缓存同步,确实可能会导致的这样的情况,比如 C 执行时 B 的结果还无法被观察到,但是设计锁的时候就要考虑到这些情况。 |
103
iseki 347 天前 via Android
@rockyliang Java 即使不使用 volatile ,使用 Lock 或者 synchronize 也会有一样的效果。
Java 对此的描述是 happens-before ,不是粗暴的 volatile 就可见别的不可见。 |
104
iseki 347 天前 via Android
volatile 所谓的可见性只是被用户总结出来的效果之一,显然不是说 volatile 只能保证这个。
|
105
Gaas6lt 347 天前 via iPhone
难道不是因为这个 chan 是不带 buffer 的吗
|
106
rockyliang OP |
107
lesismal 347 天前
@codehz #100
其实就是语言定位、取舍问题。c/cpp 这些是要把底层能力尽量留给开发者、开发者可以“肆意”掌控和进行性能优化,编译器自己优化性能的效率比肉眼要高得多。golang 的定位本来也不是像 c/cpp 那样极致性能与控制力,而是尽量在工程上让开发者能够舒服地做业务逻辑,所以写 go 也不需要考虑那么多。 |
108
zacard 347 天前
因为 channel 的同步机制是通过读写屏障,而读写屏障不是只保障 channel 里面的数据可见,它的原理是写的时候通过失效 cpu 缓存的数据,读的时候防止重排保障读到写屏障之前的数据更新。因此即使处于另一个核心的线程,由于缓存的数据失效了,会去读主存的最新数据,顺带就把最新赋值的 a 给读出来了。
java 中有很多类库直接使用了这个技巧来减少重复的同步消耗,例如 FutureTask: // 源码第 92 行 private volatile int state; // 源码第 104 行 private Object outcome; // non-volatile, protected by state reads/writes 第 2 个变量的定义没有加 voliatile ,然后可以安全的在并发中使用 |
109
CRVV 346 天前
@rockyliang
程序员写代码不需要懂这么多的东西,如果你想深究这些知识,当然可以学,这些东西都还挺有意思的,但这些知识和 “写正确的代码” 不相关。让程序员不需要懂这些东西就能写代码,就是所谓 高级语言 的功能。 计算机这东西在各个领域都是分层的,设计 HTTP 协议的人不需要懂 IP 协议要怎么工作,他只需要懂 TCP 协议就行。CPU 指令和编程语言也是类似的情况,写代码的人只需要懂编程语言,不需要懂 CPU 的工作方式,不需要懂编译器的实现细节。 重复一下,你想学当然可以学,但这些知识不能帮你把代码写对。把代码写对需要的是编程语言本身的知识。 你举的例子说 flag = false 没有生效,这件事情的原因,如果非要深究到底,那确实是这一句被编译器优化掉了。 编译器把它优化掉了,这个叫 实现细节,编译器优化掉它,是因为根据编程语言的 spec ,flag = false 这一句可以被优化掉。编译器可以优化掉它,也可以不优化。 经常出现的一种情况是,程序员写了一段带有 undefined behavier 的代码,跑了一下发现一切正常,就认为代码是对的,之后升级了编译器程序就挂了。 从编程语言的角度来说 flag = false 没有被执行到的原因,或者说编译器可以把它优化掉的原因是 这两个 for 循环执行在不同的 goroutine 上,而且 Go 没有保证 goroutine 的执行顺序,也不保证 goroutine 被执行到。 for flag print sleep 的那个循环一直占用着 CPU ,sleep 的实现是忙等,而后面的 for 循环从来都没有执行到,这是一种符合 spec 的行为。 或者 for flag print sleep 的循环要执行了一亿亿亿次以后才会执行到后面的 for ,这也符合 spec 两个 goroutine 同时对一个变量做读写操作,这个叫 data race ,当然是 undefined behavier 两个线程不能同时读写同一个变量,这个算基础知识吧 |
110
EchoGroot 345 天前
@CRVV #78 同意大佬的见解,编程语言已经封装好了这块。Go 中满足 happen befor 就可见了,不用太关心底层的缓存一致性问题。不过我认为 java 中的可见性得考虑,例如使用 volatile 。
|
111
lesismal 345 天前
@CRVV @EchoGroot #109
> for flag print sleep 的那个循环一直占用着 CPU ,sleep 的实现是忙等,而后面的 for 循环从来都没有执行到,这是一种符合 spec 的行为 sleep 的实现是忙等,这个不对吧?它可不是纯 cpu spin 那种吧? print 也是有 io 的,也不是导致后面的 for 循环从来没有执行到的原因吧? 所以虽然后面的 for 被优化掉了,但我并不清楚具体什么原因导致的优化 > 两个 goroutine 同时对一个变量做读写操作,这个叫 data race ,当然是 undefined behavier > 两个线程不能同时读写同一个变量,这个算基础知识吧 这个说法片面了吧~ data race 可能会造成不一致、undefined behavior ,但如果正确使用、并不会造成 ub 。 我代码里一些 flag 就是 data race 的,为了性能,一些简单的地方没必要都加锁,atomic 也是多余 |