Golang Go语言中读取全局变量要加锁吗
Golang Go语言中读取全局变量要加锁吗
在看同事的代码时候,发现这样的操作:
var (
object = New()
mu = sync.RWMutex
)
func SetValue(v SomeInterface) {
mu.Lock()
object = v
mu.Unlock()
}
func GetValue() SomeInterface {
mu.RLock()
defer mu.RUnlock()
return object
}
为什么要加读锁啊?!难道会 Get 到 nil ? 同事给了我这个链接: https://stackoverflow.com/questions/21447463/is-assigning-a-pointer-atomic-in-golang
更多关于Golang Go语言中读取全局变量要加锁吗的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
加锁是为了防止脏读啊。
你读了旧的数据,然而刚刚被更新,读写锁就是为了解决这个的。
更多关于Golang Go语言中读取全局变量要加锁吗的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
你这同事可以被优化掉了,这是啥代码啊
我这里的使用场景,即使是脏读也是没有问题的。问题是在 Go 里面,pointer 的赋值貌似不是原子操作的。
在这里,加锁可以解决时序问题,原子性倒是不用担心,golang 的指针操作都是原子的。
之前专门写过文章来聊 golang 里面锁到底是什么:
https://wweir.cc/post/%E6%8E%A2%E7%B4%A2-golang-%E4%B8%80%E8%87%B4%E6%80%A7%E5%8E%9F%E8%AF%AD/
代码我重新写的,去掉了其他无关的东西
如果 object 为非基本类型的,每次写都涉及到多个字段,你试试看不加锁读会不会出现部分数据已更新部分数据未更新的问题。当然,基本类型不加锁读也会出现一楼说的问题
sync/atomic 包有个 Value 结构体专门干这个
根据官方的说明,应该不是 atomic 的。
但是除非有并发的操作,我们才需要去考虑加锁,否则的话就没有必要。
Go Memory Model:
Programs that modify data being simultaneously accessed by multiple goroutines must serialize such access.
To serialize access, protect the data with channel operations or other synchronization primitives such as those in the sync and sync/atomic packages.
自己 search 了一下,有人假设一个 64bit 的 pointer,在 write 的时候,可能只写到了一半,就被另外一个线程 read 了,这时候就会 read 到一个不知道是哪里的地址……链接在这: https://stackoverflow.com/questions/41531337/is-a-read-or-write-operation-on-a-pointer-value-atomic-in-golang
官方没有说明指针操作是不是原子的,但是官方只说了一句话:Programs that modify data being simultaneously accessed by multiple goroutines must serialize such access.
链接: https://golang.org/ref/mem
瞬间觉得心理压力大,本以为是高级语言,没想到还要关心到那么细
go 的意思是让你但凡并发访问相同数据就用 sync
来保证 serialization
.
Don’t be clever. 原话
你的同事是正确的,当你犹豫是否有并发安全问题的时候,那就采用最保险的方法。
这也不是 Go 的问题,你使用 Java 也有同样的问题的
代码还有 64 位的?
你编译的时候把 GOARCH 指定为 amd64,然后代码中都假设是 64 位就行了。
不,You aren’t gonna need it, 没有并发问题就不要写并发代码。不然代码没法维护
用 Go 的现在还有非并发的程序吗?
绝大部分语言都有个定律,假如没有明确声明某个行为是并发安全的,那么它就不是。
楼主可以把便以结果打出来看下,理论上说,一次指针的等号赋值确实不是原子的,和 interface 类型没关系。人家用的是读写锁,不是互斥锁,没毛病。
并发读写加锁没问题。
个人建议写 go 代码尽量不要用 sync 包,go 的 channel 够用了,如果发现 channel 性能不够用,请考虑重新设计。尽量考虑使用消息传递。如果有能力用 sync/atomic,请忽略以上建议。
指令重排序?
如果是 x86 平台,那加不加锁都没什么关系
你只要知道你这么写会出现什么问题导致什么结果并且你觉得没问题 /业务不会出现问题就怎么写都行
目前有性能问题?
即使有性能问题也不建议移除锁,换成原子操作更好些。
官方文档未声明安全就不要这么做,鬼知道之后会是什么情况。
另外移除锁自动化测试时会报竟态冲突,过不了测试了。
https://golang.org/src/sync/atomic/value.go
看看官方的 interface{}原子操作要做多少工作
当然要加锁啊,interface 是一个指针一个类型,哪里能保证读写的原子性
何况除了原子性还有指令乱序,你不加锁,分分钟给你优化出一些会炸的顺序
死锁都好过竞态啊
何况需要保证质量时,都会开 -race 做测试,你这种并发读写的,肯定报错的
这都常识,又哪个“高级语言”不需要理解这个的?不要认为自己不懂的就叫底层!
9102 年还有人 x86 不明确加锁 /atomic 读写 64 位变量?原子性是一回事,编译器重排、指令乱序、acquire-release 语义了解一下
的确会有 data race 的问题,带 -race 跑一下代码就知道了: https://play.golang.org/p/GQ5FXw7jXe0
没觉得你同事的代码有问题, 很正常的操作. 反倒是楼上很多同学对线程安全一知半解或者漠不关心. 到时候出了奇怪的 bug 没法有规律的复现等着哭去吧
另一个角度 避免用全局变量 非要用还不加锁 这是在养 bug
一直以为赋值表达式就是原子的,看来我才疏学浅,让大神见笑了
你的说法也是对的,我一直和人说如何写好并发相关代码,那就是“尽量避免并发”
但是从这里来看,那就是需要的
没关系的,需求是可以获取旧值,只要不获取到错误的值就行
想再请教一个问题,例如我要实现一个 rpc service,我实现了一个 business struct。每个请求进来后,我要 new 一个业务逻辑对象去处理请求?还是只实例化一个对象,然后传入对象指针给每个 gorouting 去处理呢?类似的问题一般要考虑哪些点?
越看越糊涂。。
首先例子既然不用考虑旧数据问题加不加锁都不影响吧。
其次 a=b 这样是原子的吧又不是 a=b+c
颠覆认知了吧,前面大神说了,几乎没有语言能够保证,赋值是原子的。注意,这里说的是保证。
不可能吧。b+c 是因为汇编是多步的原因。
看了您的文章,非常感谢~ 干货满满
取决于这个对象调用的方法,访问的变量,是否存在竞争冲突
并发里面存在两个问题:
1. 内存可见性,从寄存器 -》 L1,L2 cache -》主内存,变量的赋值,对于其他线程,可见性是不可保证的,读取的可能是旧数据
2. 赋值本身的原子性,举个例子,消息是 32 位单位,但是需要赋值一个 int64,那么是两条 cpu 消息,分别赋值前后 32 bit 的数据,那么 a = b,a 存在一个阶段,是一种中间状态,不是之前的 a1,也不是新的值 a2
并发编程,比你想象的更难
你只需要记得一条规则:并发读写要上锁,不管读写的是什么。这样你就能避免所有的竞态问题,包括未来可能出现的。另外,很多人口里说“读到旧数据没问题”,实际绝大部分都不知道乱序执行,以为读到某个值了,就等于前面的某些指令就已经执行了,做了一些错误的假定。
我也觉得不可能啊,但事实就是这样,官方文档都说了。
我在想,为了线程安全,以后我实现的 struct,成员变量都不应该暴露了,成员变量的变更,都应该加锁。
简直颠覆认知,越来越觉得自己不配当程序员了。
不是很明白,我在考虑的是,如果每个线程都独享一个对象,那么我们是不是就“尽量避免并发”,但是这样做会以性能损失作为代价。如果多个线程共享一个对象,那么我们在实现的时候,就要考虑线程安全的问题,代价就是程序的复杂度。
我的问题是,如何作出平衡的?
楼主学过操作系统???
我还实现过 pintos 呢,学得不好,一知半解,问的问题太低级了,让各位大神见笑。
这个看你需求,以及对象本身是否“比较重”,初始化的代价等。
举个例子,spring been singleton vs prototype,就是对象是返回一个单例还是完全一个新对象
单例需要考虑并发问题,新对象不需要
谢谢,第二点这个不知道。
没毛病。。。如果 Set 跟 Get 完全没有并发操作,无所谓(那这种情下,大写一个变量不就完了。。。)
感觉有脏读问题,但至于说赋值是不是原子性的,我觉得应该是。如果你不是分拆赋值(x.a=1 x.b=2),单个地址(占用一个寄存器)的赋值操作在机器码上应该是单个指令。
在Golang(Go语言)中,是否需要对读取全局变量加锁,取决于该变量的使用场景及其并发访问情况。
-
无并发访问:如果全局变量仅在单线程或单协程中被访问和修改,那么读取时无需加锁。这种情况下,Go语言的内存模型保证了变量访问的安全性。
-
有并发访问:若全局变量在多个协程中被并发访问(包括读和写),则必须考虑同步问题。对于写操作,必须使用锁(如
sync.Mutex
或sync.RWMutex
)来确保数据一致性。对于读操作,如果只有读没有写,理论上可以不加锁(因为Go的读操作是原子的,且内存模型保证了读取到最新的值,但前提是写操作已经正确同步)。然而,在实际编程中,为了代码的清晰和避免潜在错误,推荐在读操作时也使用读锁(RWMutex
的RLock/RUnlock),特别是当代码逻辑复杂或未来可能引入写操作时。 -
最佳实践:在涉及并发访问的全局变量上,最好总是使用锁来同步读写操作,无论是读锁还是写锁,这有助于避免竞态条件和未定义行为。
总之,虽然在某些情况下,读取全局变量可能不需要加锁,但为了代码的健壮性和可维护性,建议在涉及并发访问时总是使用适当的锁机制。