问一个并发程序可见性的问题, golang 语言

2023-12-13 10:22:14 +08:00
 rockyliang

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"的呢?

12279 次点击
所在节点    Go 编程语言
111 条回复
codehz
2023-12-13 10:28:45 +08:00
channel 这里就用作读写屏障了
sunny352787
2023-12-13 10:34:52 +08:00
哪有那么复杂,就是 c 在这里锁住了当前线程不执行 print 而已,你这里的 c 是阻塞 channel ,不是非阻塞 channel ,阻塞 channel 的读取和输入你可以看作是一个操作或者类似一次函数调用,没有读取就会卡在输入这端
jonsmith
2023-12-13 10:41:55 +08:00
这个无缓冲的 channel 是同步的、会阻塞,如果缓存 channel 效果就不一样了。
leonshaw
2023-12-13 10:45:49 +08:00
原文解释的还不够清楚吗?
rockyliang
2023-12-13 10:49:17 +08:00
@sunny352787 可是就算 channel 阻塞结束了,main 协程也不一定能够打印出 "hello, world" 吧?因为 golang 是多线程,多线程并发就会有可见性问题,一个线程修改了共享变量,这个修改对于其它线程来说不一定能观测到
AnroZ
2023-12-13 10:55:43 +08:00
这个思考挺有意思的。
我猜是 channel 阻塞恢复后会触发一次协程的上下文同步,即使跑在不同核不同物理线程上也是被强制同步了一次,所以这个例子是可以保证打印出"hello, world" 的。
InDom
2023-12-13 11:02:58 +08:00
既然是无缓冲的,而且 chan 的特性是会阻塞。

也就是,不管是谁先执行的,都会等 main 把 c <- 0 并且 g f() 里面取到到 <-c 以后在继续执行。

这里有疑问的无非是 main 比 f 先执行,那么 c <-0 会等待,等 f 执行完以后才会继续。
kiddingU
2023-12-13 11:05:39 +08:00
搞明白这里 channel 的作用你就知道为啥了
kiddingU
2023-12-13 11:08:14 +08:00
<-ch 读阻塞 ,go f ,必定是先执行 a 的赋值了,c <- 0 写入之后才能执行到 print
rockyliang
2023-12-13 11:08:18 +08:00
@InDom 谢谢回答,我知道无缓冲的 channel 会阻塞,但我关注的重点是多线程并发的可见性问题,具体可以看 #5 的回复
PTLin
2023-12-13 11:15:24 +08:00
了解原子变量的话,把 channel 读写的地方当作有一致内存序就好了。
lifei6671
2023-12-13 11:16:35 +08:00
感觉你想知道的是 golang 中的内存一致性模型是怎么实现的。
golang 中经常讨论的一个概念是 happens before ,
这段代码就完美诠释了 happens before 的精髓,其中针对一个无缓冲的 channel 来说,它的时序依赖如下:
如果 ch 是一个 unbuffered channel 则,ch<-val > val <- ch
也就是从 channel 接受数据 happens before 往 channel 写数据。根据 happens before 的传导性就可以推断出,后面的读 a 变量 happens before 写 a 变量。
wei2629
2023-12-13 11:17:43 +08:00
协程无法保证顺序,chan 不就是 为了保证 print(a) 在 f() 之后执行吗? a = "hello, world" 修改了内存, 当 print 打印内存 就没问题了。
sunny352787
2023-12-13 11:18:25 +08:00
老弟啊,咱们写程序修改的变量都是在内存里的呀,谁去管缓存干什么了,修改了那对所有的线程都是可见的呀,多线程会出现的问题是修改一个变量的顺序,而不是修改完不给别人...
yph007595
2023-12-13 11:19:10 +08:00
@rockyliang #5 一个线程修改了共享变量,为什么对其他现成不一定能观测到?如果观测不到,那程序不乱套了么,都不能保证程序的正确性了。
xuanbg
2023-12-13 11:19:28 +08:00
要是程序需要这么写才能正常运行,那这个程序就是臭狗屎。无缓冲的 channel 会阻塞,和多线程并发的可见性是两个特性,千万别搅在一起用。
godgrp
2023-12-13 11:20:16 +08:00
线程、协程不同的吧
ZField
2023-12-13 11:24:14 +08:00
@yph007595 #15 大胆猜测下他的思考是基于 jvm 的本地内存,在本地内存里面会有变量的副本,所以会存在可见性的问题
cyrivlclth
2023-12-13 11:24:23 +08:00
#5 说的依据是什么?其他线程能不能观测到和这个问题没啥关系吧,他们又不是不可预见的发生顺序,而是加了阻塞 channel ,在 go 中,修改全局变量一定发生在 print 之前。
rockyliang
2023-12-13 11:28:28 +08:00
@yph007595 #15 因为 CPU 有多个核心,每个核心都有独立的缓存,线程 A 修改了共享变量,那么这个修改只会存储到它自己所在的核心,跑在其它 CPU 核心的线程不一定能知道这个修改,比如 java 语言提供的 volatile 关键字,就是通过禁用 CPU 缓存来解决这个可见性问题的(最后一句关于 java 的话来自 ChatGPT )

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/999936

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX