sync包once小问题

goCurrent · 2023-09-13 23:55:27 · 3052 次点击 · 大约8小时之前 开始浏览    置顶
这是一个创建于 2023-09-13 23:55:27 的主题,其中的信息可能已经有所发展或是发生改变。

Do函数里面 刚开始进来的时候,直接用0.done == 0 判断也能达到效果,atomic.LoadUint32(&o.done) == 0目的是啥,为了很多goroutine同时调用Do函数,减少o.m.Lock()的调用吗


有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

3052 次点击  
加入收藏 微博
18 回复  |  直到 2023-09-22 23:00:10
kingJ
kingJ · #1 · 2年之前

线程安全

goCurrent
goCurrent · #2 · 2年之前

@kingJ 线程安全是指啥,两个goroutine同时读一个int应该不会panic

zzustu
zzustu · #3 · 2年之前

你的疑问也正是我的疑问,

kingJ
kingJ · #4 · 2年之前

atomic.StoreUint32(&o.done, 1) 和 atomic.LoadUint32(&o.done) 应是保证数据一致性的

goCurrent
goCurrent · #5 · 2年之前
kingJkingJ #4 回复

atomic.StoreUint32(&o.done, 1) 和 atomic.LoadUint32(&o.done) 应是保证数据一致性的

但是程序逻辑性其实不需要原子操作,因为有锁

kingJ
kingJ · #6 · 2年之前
goCurrentgoCurrent #5 回复

#4楼 @kingJ 但是程序逻辑性其实不需要原子操作,因为有锁

最开始读o.done的时候并不在锁中,看看别人的文章 https://zhuanlan.zhihu.com/p/520607098

lxzan
lxzan · #7 · 2年之前
goCurrentgoCurrent #5 回复

#4楼 @kingJ 但是程序逻辑性其实不需要原子操作,因为有锁

注释里面有说明, 为什么有了原子操作还加锁

zzustu
zzustu · #8 · 2年之前
lxzanlxzan #7 回复

#5楼 @goCurrent 注释里面有说明, 为什么有了原子操作还加锁

疑问不在于为什么有了原子操作还要加锁,而是有了加锁还要用原子操作, 如下写法会有线程安全问题吗?

type Once struct {
    done uint32
    m    Mutex
}

func (o *Once) Do(f func()) {
    if o.done == 0 {
        o.doSlow(f)
    }
}

func (o *Once) doSlow(f func()) {
    o.m.Lock()
    defer o.m.Unlock()
    if o.done == 0 {
        defer o.done = 1
        f()
    }
}
zzustu
zzustu · #9 · 2年之前

还有一个疑问就是既然用了 atomic 为何又没贯彻 atomicdoSlow 锁里面为何用 if o.done == 0 而不是 if atomic.LoadUint32(&o.done) == 0

lxzan
lxzan · #10 · 2年之前
zzustuzzustu #8 回复

#7楼 @lxzan 疑问不在于为什么有了原子操作还要加锁,而是有了加锁还要用原子操作, 如下写法会有线程安全问题吗? ```go type Once struct { done uint32 m Mutex } func (o *Once) Do(f func()) { if o.done == 0 { o.doSlow(f) } } func (o *Once) doSlow(f func()) { o.m.Lock() defer o.m.Unlock() if o.done == 0 { defer o.done = 1 f() } } ```

可能会出现unexpected result

wuxq
wuxq · #11 · 2年之前

如果是多goroutine 情况下, o.done 和 atomic.LoadUint32(&o.done) 区别:

1、o.done的读取在执行上可能需要好几次 cpu 指令操作,这几次 cpu 指令执行中,可能有别的 goroutine 在修改这个 done 的值,导致你读取到的可能是一个不确定的值 2、用 atomic 这种原子操作,cpu 指令在执行中会保证别的goroutine不去修改这个值,所以读取到的是一个确定的值;

建议看一下 go 的内存模型: https://go.dev/ref/mem

yzbzg
yzbzg · #12 · 2年之前

那是原子性操作,也是线程安全的,理论上原子性操作会比锁的性能高一些

jiongge
jiongge · #13 · 2年之前

1

zzustu
zzustu · #14 · 2年之前
wuxqwuxq #11 回复

如果是多goroutine 情况下, o.done 和 atomic.LoadUint32(&o.done) 区别: 1、o.done的读取在执行上可能需要好几次 cpu 指令操作,这几次 cpu 指令执行中,可能有别的 goroutine 在修改这个 done 的值,导致你读取到的可能是一个不确定的值 2、用 atomic 这种原子操作,cpu 指令在执行中会保证别的goroutine不去修改这个值,所以读取到的是一个确定的值; 建议看一下 go 的内存模型: https://go.dev/ref/mem

感谢解答,其实道理我能理解,但是就代码论代码,如果 sync.Once 写成如下这样,会有线程安全问题吗?如果有,会哪些地方有线程安全问题。

type Once struct {
    done uint32
    m    Mutex
}

func (o *Once) Do(f func()) {
    if o.done == 0 {
        o.doSlow(f)
    }
}

func (o *Once) doSlow(f func()) {
    o.m.Lock()
    defer o.m.Unlock()
    if o.done == 0 {
        defer o.done = 1
        f()
    }
}
lxzan
lxzan · #15 · 2年之前
zzustuzzustu #14 回复

#11楼 @wuxq 感谢解答,其实道理我能理解,但是就代码论代码,如果 sync.Once 写成如下这样,会有线程安全问题吗?如果有,会哪些地方有线程安全问题。 ```go type Once struct { done uint32 m Mutex } func (o *Once) Do(f func()) { if o.done == 0 { o.doSlow(f) } } func (o *Once) doSlow(f func()) { o.m.Lock() defer o.m.Unlock() if o.done == 0 { defer o.done = 1 f() } } ```

感觉没毛病

jan-bar
jan-bar · #16 · 2年之前
zzustuzzustu #14 回复

#11楼 @wuxq 感谢解答,其实道理我能理解,但是就代码论代码,如果 sync.Once 写成如下这样,会有线程安全问题吗?如果有,会哪些地方有线程安全问题。 ```go type Once struct { done uint32 m Mutex } func (o *Once) Do(f func()) { if o.done == 0 { o.doSlow(f) } } func (o *Once) doSlow(f func()) { o.m.Lock() defer o.m.Unlock() if o.done == 0 { defer o.done = 1 f() } } ```

以前的大佬给出的解释,而且看官方注释也有提到内存模型问题:https://studygolang.com/articles/25299

zzustu
zzustu · #17 · 2年之前
jan-barjan-bar #16 回复

#14楼 @zzustu 以前的大佬给出的解释,而且看官方注释也有提到内存模型问题:https://studygolang.com/articles/25299

感谢,这个答案基本上回答了我所有的疑问

goCurrent
goCurrent · #18 · 2年之前
wuxqwuxq #11 回复

如果是多goroutine 情况下, o.done 和 atomic.LoadUint32(&o.done) 区别: 1、o.done的读取在执行上可能需要好几次 cpu 指令操作,这几次 cpu 指令执行中,可能有别的 goroutine 在修改这个 done 的值,导致你读取到的可能是一个不确定的值 2、用 atomic 这种原子操作,cpu 指令在执行中会保证别的goroutine不去修改这个值,所以读取到的是一个确定的值; 建议看一下 go 的内存模型: https://go.dev/ref/mem

谢谢回答。我也知道能确定,但是问题是不用原子对逻辑正确性没有影响,因为有锁保证。benchmark的话会发现性能也没区别。最终只能想是为了多操作系统不同底层代码和不同go版本的适配性。

添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传