在看同事的代码时候,发现这样的操作:
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
1
seaguest 2019-03-13 16:59:28 +08:00
加锁是为了防止脏读啊。
你读了旧的数据,然而刚刚被更新,读写锁就是为了解决这个的。 |
2
xkeyideal 2019-03-13 17:04:19 +08:00
你这同事可以被优化掉了,这是啥代码啊
|
3
index90 OP @seaguest 我这里的使用场景,即使是脏读也是没有问题的。问题是在 Go 里面,pointer 的赋值貌似不是原子操作的。
|
4
wweir 2019-03-13 17:08:37 +08:00 1
在这里,加锁可以解决时序问题,原子性倒是不用担心,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/ |
6
LANB0 2019-03-13 17:14:12 +08:00
如果 object 为非基本类型的,每次写都涉及到多个字段,你试试看不加锁读会不会出现部分数据已更新部分数据未更新的问题。当然,基本类型不加锁读也会出现一楼说的问题
|
7
zhujinliang 2019-03-13 17:17:09 +08:00 via iPhone
sync/atomic 包有个 Value 结构体专门干这个
|
8
seaguest 2019-03-13 17:42:15 +08:00 1
根据官方的说明,应该不是 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. |
9
index90 OP 自己 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 |
11
keakon 2019-03-13 17:55:24 +08:00 1
如果不考虑兼容性和可移植性,只写 64 位的代码就行了。编译器会做内存对齐,在 64 位机器上操作 64 位的指针或整数是原子的。
|
12
justfly 2019-03-13 18:06:54 +08:00 1
|
13
cloudzhou 2019-03-13 18:07:23 +08:00 1
你的同事是正确的,当你犹豫是否有并发安全问题的时候,那就采用最保险的方法。
这也不是 Go 的问题,你使用 Java 也有同样的问题的 |
16
lihongjie0209 2019-03-13 19:04:11 +08:00
@cloudzhou 不,You aren't gonna need it, 没有并发问题就不要写并发代码。不然代码没法维护
|
17
index90 OP @lihongjie0209 用 Go 的现在还有非并发的程序吗?
|
18
fengjianxinghun 2019-03-13 19:16:48 +08:00 1
绝大部分语言都有个定律,假如没有明确声明某个行为是并发安全的,那么它就不是。
|
19
kkeiko 2019-03-13 19:21:43 +08:00
楼主可以把便以结果打出来看下,理论上说,一次指针的等号赋值确实不是原子的,和 interface 类型没关系。人家用的是读写锁,不是互斥锁,没毛病。
|
20
henglinli 2019-03-13 19:28:06 +08:00 via iPhone
并发读写加锁没问题。
个人建议写 go 代码尽量不要用 sync 包,go 的 channel 够用了,如果发现 channel 性能不够用,请考虑重新设计。尽量考虑使用消息传递。如果有能力用 sync/atomic,请忽略以上建议。 |
21
qiyuey 2019-03-13 19:32:50 +08:00 via Android
指令重排序?
|
22
hilbertz 2019-03-13 19:36:27 +08:00
如果是 x86 平台,那加不加锁都没什么关系
|
23
janxin 2019-03-13 19:36:50 +08:00
你只要知道你这么写会出现什么问题导致什么结果并且你觉得没问题 /业务不会出现问题就怎么写都行
|
24
gamexg 2019-03-13 20:19:52 +08:00
目前有性能问题?
即使有性能问题也不建议移除锁,换成原子操作更好些。 官方文档未声明安全就不要这么做,鬼知道之后会是什么情况。 另外移除锁自动化测试时会报竟态冲突,过不了测试了。 |
25
fengjianxinghun 2019-03-13 20:41:33 +08:00 via iPhone
|
26
ms2008 2019-03-13 21:37:45 +08:00 1
|
27
blless 2019-03-13 21:52:48 +08:00 via Android
看场景吧,大部分全局变量都是直接读…除非会导致业务脏数据才需要锁配合
|
28
fengjianxinghun 2019-03-13 23:00:54 +08:00 via iPhone
@keakon 单 cpu 可以有这种保证,smp 下就不行了,得汇编 lock 指令总线,这也是原子 atomic 得由来
|
29
reus 2019-03-13 23:39:39 +08:00 2
当然要加锁啊,interface 是一个指针一个类型,哪里能保证读写的原子性
何况除了原子性还有指令乱序,你不加锁,分分钟给你优化出一些会炸的顺序 死锁都好过竞态啊 何况需要保证质量时,都会开 -race 做测试,你这种并发读写的,肯定报错的 这都常识,又哪个“高级语言”不需要理解这个的?不要认为自己不懂的就叫底层! |
30
yanaraika 2019-03-13 23:51:12 +08:00 via Android
9102 年还有人 x86 不明确加锁 /atomic 读写 64 位变量?原子性是一回事,编译器重排、指令乱序、acquire-release 语义了解一下
|
31
mornlight 2019-03-14 00:28:45 +08:00
的确会有 data race 的问题,带 -race 跑一下代码就知道了: https://play.golang.org/p/GQ5FXw7jXe0
|
32
zkeeper 2019-03-14 06:10:41 +08:00
没觉得你同事的代码有问题, 很正常的操作. 反倒是楼上很多同学对线程安全一知半解或者漠不关心. 到时候出了奇怪的 bug 没法有规律的复现等着哭去吧
|
33
vindurriel 2019-03-14 07:12:25 +08:00 via iPhone
另一个角度 避免用全局变量 非要用还不加锁 这是在养 bug
|
35
cloudzhou 2019-03-14 10:05:18 +08:00
@lihongjie0209 你的说法也是对的,我一直和人说如何写好并发相关代码,那就是“尽量避免并发”
但是从这里来看,那就是需要的 |
36
keakon 2019-03-14 10:21:47 +08:00
@fengjianxinghun 没关系的,需求是可以获取旧值,只要不获取到错误的值就行
|
37
index90 OP @cloudzhou 想再请教一个问题,例如我要实现一个 rpc service,我实现了一个 business struct。每个请求进来后,我要 new 一个业务逻辑对象去处理请求?还是只实例化一个对象,然后传入对象指针给每个 gorouting 去处理呢?类似的问题一般要考虑哪些点?
|
38
zarte 2019-03-14 10:22:59 +08:00
越看越糊涂。。
首先例子既然不用考虑旧数据问题加不加锁都不影响吧。 其次 a=b 这样是原子的吧又不是 a=b+c |
42
cloudzhou 2019-03-14 10:37:40 +08:00 3
|
43
reus 2019-03-14 10:39:48 +08:00
@zarte 你只需要记得一条规则:并发读写要上锁,不管读写的是什么。这样你就能避免所有的竞态问题,包括未来可能出现的。另外,很多人口里说“读到旧数据没问题”,实际绝大部分都不知道乱序执行,以为读到某个值了,就等于前面的某些指令就已经执行了,做了一些错误的假定。
|
44
index90 OP @zarte 我也觉得不可能啊,但事实就是这样,官方文档都说了。
我在想,为了线程安全,以后我实现的 struct,成员变量都不应该暴露了,成员变量的变更,都应该加锁。 简直颠覆认知,越来越觉得自己不配当程序员了。 |
45
index90 OP @cloudzhou 不是很明白,我在考虑的是,如果每个线程都独享一个对象,那么我们是不是就“尽量避免并发”,但是这样做会以性能损失作为代价。如果多个线程共享一个对象,那么我们在实现的时候,就要考虑线程安全的问题,代价就是程序的复杂度。
我的问题是,如何作出平衡的? |
46
tt67wq 2019-03-14 10:49:59 +08:00
楼主学过操作系统???
|
48
cloudzhou 2019-03-14 10:53:13 +08:00 1
@index90 这个看你需求,以及对象本身是否“比较重”,初始化的代价等。
举个例子,spring been singleton vs prototype,就是对象是返回一个单例还是完全一个新对象 单例需要考虑并发问题,新对象不需要 |
50
sdrzlyz 2019-03-14 11:22:38 +08:00
没毛病。。。如果 Set 跟 Get 完全没有并发操作,无所谓(那这种情下,大写一个变量不就完了。。。)
|
51
ihciah 2019-03-14 11:43:08 +08:00 via iPhone
感觉有脏读问题,但至于说赋值是不是原子性的,我觉得应该是。如果你不是分拆赋值(x.a=1 x.b=2),单个地址(占用一个寄存器)的赋值操作在机器码上应该是单个指令。
|