V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
index90
V2EX  ›  Go 编程语言

Go 读取全局变量要加锁?!

  •  
  •   index90 · 2019-03-13 16:48:33 +08:00 · 10981 次点击
    这是一个创建于 2080 天前的主题,其中的信息可能已经有所发展或是发生改变。

    在看同事的代码时候,发现这样的操作:

    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 条附言  ·  2019-03-13 17:23:31 +08:00
    看来我没有说清楚
    1. 首先这里被操作的变量是一个 interface 类型,我对它的理解基本就是指针。
    2. 使用场景不存在脏读或脏写问题,即使 Get 到旧值也没有关系。
    3. 问题焦点在于 interface 类型的赋值是不是原子操作。

    看来都不看我发的链接,我复制出来吧:
    Question:
    Is assigning a pointer atomic in go?

    Do I need to assign a pointer in a lock? Suppose I just want to assign the pointer to nil, and would like other threads to be able to see it. I know in java we can use volatile for this. But there is no volatile in go.

    Thanks.

    Answer:
    The only things which are guaranteed to be atomic in go are the operations in sync.atomic.

    ……
    第 2 条附言  ·  2019-03-14 09:56:23 +08:00
    我这里想说的,不是说我同事代码没有问题,而且他这样写是正确的。
    我这里想表达的是,写了那么多年代码,一直以为一个赋值表达式是原子性的,没想到自己是多么的无知。
    感慨之余,和大家分享一下。
    51 条回复    2019-03-14 11:43:08 +08:00
    seaguest
        1
    seaguest  
       2019-03-13 16:59:28 +08:00
    加锁是为了防止脏读啊。
    你读了旧的数据,然而刚刚被更新,读写锁就是为了解决这个的。
    xkeyideal
        2
    xkeyideal  
       2019-03-13 17:04:19 +08:00
    你这同事可以被优化掉了,这是啥代码啊
    index90
        3
    index90  
    OP
       2019-03-13 17:07:38 +08:00
    @seaguest 我这里的使用场景,即使是脏读也是没有问题的。问题是在 Go 里面,pointer 的赋值貌似不是原子操作的。
    wweir
        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/
    index90
        5
    index90  
    OP
       2019-03-13 17:08:45 +08:00
    @xkeyideal 代码我重新写的,去掉了其他无关的东西
    LANB0
        6
    LANB0  
       2019-03-13 17:14:12 +08:00
    如果 object 为非基本类型的,每次写都涉及到多个字段,你试试看不加锁读会不会出现部分数据已更新部分数据未更新的问题。当然,基本类型不加锁读也会出现一楼说的问题
    zhujinliang
        7
    zhujinliang  
       2019-03-13 17:17:09 +08:00 via iPhone
    sync/atomic 包有个 Value 结构体专门干这个
    seaguest
        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.
    index90
        9
    index90  
    OP
       2019-03-13 17:44:38 +08:00   ❤️ 1
    自己 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
    index90
        10
    index90  
    OP
       2019-03-13 17:47:05 +08:00
    @seaguest 瞬间觉得心理压力大,本以为是高级语言,没想到还要关心到那么细
    keakon
        11
    keakon  
       2019-03-13 17:55:24 +08:00   ❤️ 1
    如果不考虑兼容性和可移植性,只写 64 位的代码就行了。编译器会做内存对齐,在 64 位机器上操作 64 位的指针或整数是原子的。
    justfly
        12
    justfly  
       2019-03-13 18:06:54 +08:00   ❤️ 1
    @index90 go 的意思是让你但凡并发访问相同数据就用 `sync` 来保证 `serialization`.

    Don't be clever. 原话
    cloudzhou
        13
    cloudzhou  
       2019-03-13 18:07:23 +08:00   ❤️ 1
    你的同事是正确的,当你犹豫是否有并发安全问题的时候,那就采用最保险的方法。
    这也不是 Go 的问题,你使用 Java 也有同样的问题的
    tulongtou
        14
    tulongtou  
       2019-03-13 18:21:21 +08:00
    @keakon 代码还有 64 位的?
    keakon
        15
    keakon  
       2019-03-13 18:55:50 +08:00
    @tulongtou 你编译的时候把 GOARCH 指定为 amd64,然后代码中都假设是 64 位就行了。
    lihongjie0209
        16
    lihongjie0209  
       2019-03-13 19:04:11 +08:00
    @cloudzhou 不,You aren't gonna need it, 没有并发问题就不要写并发代码。不然代码没法维护
    index90
        17
    index90  
    OP
       2019-03-13 19:12:33 +08:00
    @lihongjie0209 用 Go 的现在还有非并发的程序吗?
    fengjianxinghun
        18
    fengjianxinghun  
       2019-03-13 19:16:48 +08:00   ❤️ 1
    绝大部分语言都有个定律,假如没有明确声明某个行为是并发安全的,那么它就不是。
    kkeiko
        19
    kkeiko  
       2019-03-13 19:21:43 +08:00
    楼主可以把便以结果打出来看下,理论上说,一次指针的等号赋值确实不是原子的,和 interface 类型没关系。人家用的是读写锁,不是互斥锁,没毛病。
    henglinli
        20
    henglinli  
       2019-03-13 19:28:06 +08:00 via iPhone
    并发读写加锁没问题。
    个人建议写 go 代码尽量不要用 sync 包,go 的 channel 够用了,如果发现 channel 性能不够用,请考虑重新设计。尽量考虑使用消息传递。如果有能力用 sync/atomic,请忽略以上建议。
    qiyuey
        21
    qiyuey  
       2019-03-13 19:32:50 +08:00 via Android
    指令重排序?
    hilbertz
        22
    hilbertz  
       2019-03-13 19:36:27 +08:00
    如果是 x86 平台,那加不加锁都没什么关系
    janxin
        23
    janxin  
       2019-03-13 19:36:50 +08:00
    你只要知道你这么写会出现什么问题导致什么结果并且你觉得没问题 /业务不会出现问题就怎么写都行
    gamexg
        24
    gamexg  
       2019-03-13 20:19:52 +08:00
    目前有性能问题?
    即使有性能问题也不建议移除锁,换成原子操作更好些。

    官方文档未声明安全就不要这么做,鬼知道之后会是什么情况。
    另外移除锁自动化测试时会报竟态冲突,过不了测试了。
    fengjianxinghun
        25
    fengjianxinghun  
       2019-03-13 20:41:33 +08:00 via iPhone
    https://golang.org/src/sync/atomic/value.go

    看看官方的 interface{}原子操作要做多少工作
    blless
        27
    blless  
       2019-03-13 21:52:48 +08:00 via Android
    看场景吧,大部分全局变量都是直接读…除非会导致业务脏数据才需要锁配合
    fengjianxinghun
        28
    fengjianxinghun  
       2019-03-13 23:00:54 +08:00 via iPhone
    @keakon 单 cpu 可以有这种保证,smp 下就不行了,得汇编 lock 指令总线,这也是原子 atomic 得由来
    reus
        29
    reus  
       2019-03-13 23:39:39 +08:00   ❤️ 2
    当然要加锁啊,interface 是一个指针一个类型,哪里能保证读写的原子性
    何况除了原子性还有指令乱序,你不加锁,分分钟给你优化出一些会炸的顺序
    死锁都好过竞态啊
    何况需要保证质量时,都会开 -race 做测试,你这种并发读写的,肯定报错的
    这都常识,又哪个“高级语言”不需要理解这个的?不要认为自己不懂的就叫底层!
    yanaraika
        30
    yanaraika  
       2019-03-13 23:51:12 +08:00 via Android
    9102 年还有人 x86 不明确加锁 /atomic 读写 64 位变量?原子性是一回事,编译器重排、指令乱序、acquire-release 语义了解一下
    mornlight
        31
    mornlight  
       2019-03-14 00:28:45 +08:00
    的确会有 data race 的问题,带 -race 跑一下代码就知道了: https://play.golang.org/p/GQ5FXw7jXe0
    zkeeper
        32
    zkeeper  
       2019-03-14 06:10:41 +08:00
    没觉得你同事的代码有问题, 很正常的操作. 反倒是楼上很多同学对线程安全一知半解或者漠不关心. 到时候出了奇怪的 bug 没法有规律的复现等着哭去吧
    vindurriel
        33
    vindurriel  
       2019-03-14 07:12:25 +08:00 via iPhone
    另一个角度 避免用全局变量 非要用还不加锁 这是在养 bug
    index90
        34
    index90  
    OP
       2019-03-14 09:58:40 +08:00
    @reus 一直以为赋值表达式就是原子的,看来我才疏学浅,让大神见笑了
    cloudzhou
        35
    cloudzhou  
       2019-03-14 10:05:18 +08:00
    @lihongjie0209 你的说法也是对的,我一直和人说如何写好并发相关代码,那就是“尽量避免并发”
    但是从这里来看,那就是需要的
    keakon
        36
    keakon  
       2019-03-14 10:21:47 +08:00
    @fengjianxinghun 没关系的,需求是可以获取旧值,只要不获取到错误的值就行
    index90
        37
    index90  
    OP
       2019-03-14 10:22:52 +08:00
    @cloudzhou 想再请教一个问题,例如我要实现一个 rpc service,我实现了一个 business struct。每个请求进来后,我要 new 一个业务逻辑对象去处理请求?还是只实例化一个对象,然后传入对象指针给每个 gorouting 去处理呢?类似的问题一般要考虑哪些点?
    zarte
        38
    zarte  
       2019-03-14 10:22:59 +08:00
    越看越糊涂。。
    首先例子既然不用考虑旧数据问题加不加锁都不影响吧。
    其次 a=b 这样是原子的吧又不是 a=b+c
    index90
        39
    index90  
    OP
       2019-03-14 10:26:31 +08:00
    @zarte 颠覆认知了吧,前面大神说了,几乎没有语言能够保证,赋值是原子的。注意,这里说的是保证。
    zarte
        40
    zarte  
       2019-03-14 10:28:42 +08:00
    @index90 不可能吧。b+c 是因为汇编是多步的原因。
    richzhu
        41
    richzhu  
       2019-03-14 10:35:41 +08:00
    @wweir 看了您的文章,非常感谢~ 干货满满
    cloudzhou
        42
    cloudzhou  
       2019-03-14 10:37:40 +08:00   ❤️ 3
    @index90 取决于这个对象调用的方法,访问的变量,是否存在竞争冲突

    @zarte 并发里面存在两个问题:
    1. 内存可见性,从寄存器 -》 L1,L2 cache -》主内存,变量的赋值,对于其他线程,可见性是不可保证的,读取的可能是旧数据
    2. 赋值本身的原子性,举个例子,消息是 32 位单位,但是需要赋值一个 int64,那么是两条 cpu 消息,分别赋值前后 32 bit 的数据,那么 a = b,a 存在一个阶段,是一种中间状态,不是之前的 a1,也不是新的值 a2

    并发编程,比你想象的更难
    reus
        43
    reus  
       2019-03-14 10:39:48 +08:00
    @zarte 你只需要记得一条规则:并发读写要上锁,不管读写的是什么。这样你就能避免所有的竞态问题,包括未来可能出现的。另外,很多人口里说“读到旧数据没问题”,实际绝大部分都不知道乱序执行,以为读到某个值了,就等于前面的某些指令就已经执行了,做了一些错误的假定。
    index90
        44
    index90  
    OP
       2019-03-14 10:42:24 +08:00
    @zarte 我也觉得不可能啊,但事实就是这样,官方文档都说了。
    我在想,为了线程安全,以后我实现的 struct,成员变量都不应该暴露了,成员变量的变更,都应该加锁。
    简直颠覆认知,越来越觉得自己不配当程序员了。
    index90
        45
    index90  
    OP
       2019-03-14 10:48:14 +08:00
    @cloudzhou 不是很明白,我在考虑的是,如果每个线程都独享一个对象,那么我们是不是就“尽量避免并发”,但是这样做会以性能损失作为代价。如果多个线程共享一个对象,那么我们在实现的时候,就要考虑线程安全的问题,代价就是程序的复杂度。
    我的问题是,如何作出平衡的?
    tt67wq
        46
    tt67wq  
       2019-03-14 10:49:59 +08:00
    楼主学过操作系统???
    index90
        47
    index90  
    OP
       2019-03-14 10:52:58 +08:00
    @tt67wq 我还实现过 pintos 呢,学得不好,一知半解,问的问题太低级了,让各位大神见笑。
    cloudzhou
        48
    cloudzhou  
       2019-03-14 10:53:13 +08:00   ❤️ 1
    @index90 这个看你需求,以及对象本身是否“比较重”,初始化的代价等。
    举个例子,spring been singleton vs prototype,就是对象是返回一个单例还是完全一个新对象
    单例需要考虑并发问题,新对象不需要
    zarte
        49
    zarte  
       2019-03-14 11:13:50 +08:00
    @cloudzhou 谢谢,第二点这个不知道。
    sdrzlyz
        50
    sdrzlyz  
       2019-03-14 11:22:38 +08:00
    没毛病。。。如果 Set 跟 Get 完全没有并发操作,无所谓(那这种情下,大写一个变量不就完了。。。)
    ihciah
        51
    ihciah  
       2019-03-14 11:43:08 +08:00 via iPhone
    感觉有脏读问题,但至于说赋值是不是原子性的,我觉得应该是。如果你不是分拆赋值(x.a=1 x.b=2),单个地址(占用一个寄存器)的赋值操作在机器码上应该是单个指令。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5455 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 03:25 · PVG 11:25 · LAX 19:25 · JFK 22:25
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.