draveness / blog-comments

面向信仰编程
https://draveness.me
140 stars 6 forks source link

Golang 并发编程与同步原语 · /golang-sync-primitives #151

Closed draveness closed 2 years ago

draveness commented 5 years ago

https://draveness.me/golang-sync-primitives

draveness commented 3 years ago
  • go语言中"*"操作是原子操作吗?

是原子操作

  • go语言中"*"操作可以确保实现内存读屏障吗?

Go 语言没有内存读屏障

mncu commented 3 years ago

@draveness

  • go语言中"*"操作是原子操作吗?

是原子操作

  • go语言中"*"操作可以确保实现内存读屏障吗?

Go 语言没有内存读屏障

感谢回复。

关于第二点,我在看go的源码时,看到:

// runtime/proc.go 
func runqput(_p_ *p, gp *g, next bool) {
        ...
retry:
    h := atomic.LoadAcq(&_p_.runqhead) // load-acquire, synchronize with consumers
    t := _p_.runqtail
    if t-h < uint32(len(_p_.runq)) {
        _p_.runq[t%uint32(len(_p_.runq))].set(gp)
        atomic.StoreRel(&_p_.runqtail, t+1) // store-release, makes the item available for consumption
        return
    }
    ......
}

在注释中出现了load-acquirestore-release,我所认知的load-acquirestore-release就是分别对应了读写屏障(是我的理解有问题?如果是还请帮忙指正),所以根据这个我才去看了atomic中LoadAcq的源码,发现在amd64架构下其仅仅只是用了“*”操作。

对于runqput我知道:

我现在想问:

douglarek commented 3 years ago

Once.Do 展开了

if atomic.LoadUint32(&o.done) == 0 {
  // Outlined slow-path to allow inlining of the fast-path.
  // o.doSlow(f)
    {
        o.m.Lock()
        defer o.m.Unlock()
        if o.done == 0 {
      defer atomic.StoreUint32(&o.done, 1)
          f()
        }
    }
}

这里使用mutex 有锁的双重检查的味道,为什么还需要使用原子操作呢?

理论上可以如下面代码这样(因为 defer 那个地方其实是被 lock 包裹的,但是受限于现阶段的 go race 机制不能了),第一个 LoadUint32 是必须的,直接 if o.done == 0 不行,因为不是原子操作那么 go race detect 会检测到:

func (o *Once) Do(f func()) {
    // Note: Here is an incorrect implementation of Do:
    //
    //  if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
    //      f()
    //  }
    //
    // Do guarantees that when it returns, f has finished.
    // This implementation would not implement that guarantee:
    // given two simultaneous calls, the winner of the cas would
    // call f, and the second would return immediately, without
    // waiting for the first's call to f to complete.
    // This is why the slow path falls back to a mutex, and why
    // the atomic.StoreUint32 must be delayed until after f returns.

    if atomic.LoadUint32(&o.done) == 0 {
        // Outlined slow-path to allow inlining of the fast-path.
        o.doSlow(f)
    }
}

func (o *Once) doSlow(f func()) {
    o.m.Lock()
    defer o.m.Unlock()
    if o.done == 0 {
        // defer atomic.StoreUint32(&o.done, 1)
        defer func() {o.done++}()
        f()
    }
}
ningmonguo commented 3 years ago

大佬请教一下,我看读写锁的代码的时候,发现一批读锁和写锁竞争的时候。为了描述清楚我分步描述一下。 1,读锁拿到了锁。 2,写锁拿到了写锁阻塞其他写,然后被读锁阻塞。 3,后续的获取读锁操作判断 被写锁修改为了负数的readcount < 0,于是都拿不到读锁了。 我的问题是,读锁下阻塞写,不应该阻塞其他读呀,这样的话就阻塞了其他的读了,这样做会不会有问题呢?并发请求锁的场景下。

draveness commented 3 years ago

大佬请教一下,我看读写锁的代码的时候,发现一批读锁和写锁竞争的时候。为了描述清楚我分步描述一下。 1,读锁拿到了锁。 2,写锁拿到了写锁阻塞其他写,然后被读锁阻塞。 3,后续的获取读锁操作判断 被写锁修改为了负数的readcount < 0,于是都拿不到读锁了。 我的问题是,读锁下阻塞写,不应该阻塞其他读呀,这样的话就阻塞了其他的读了,这样做会不会有问题呢?并发请求锁的场景下。

你不阻塞其他读的话,那写锁要等太久了,可能会被饿死

ningmonguo commented 3 years ago

@draveness

大佬请教一下,我看读写锁的代码的时候,发现一批读锁和写锁竞争的时候。为了描述清楚我分步描述一下。 1,读锁拿到了锁。 2,写锁拿到了写锁阻塞其他写,然后被读锁阻塞。 3,后续的获取读锁操作判断 被写锁修改为了负数的readcount < 0,于是都拿不到读锁了。 我的问题是,读锁下阻塞写,不应该阻塞其他读呀,这样的话就阻塞了其他的读了,这样做会不会有问题呢?并发请求锁的场景下。

你不阻塞其他读的话,那写锁要等太久了,可能会被饿死

嗷嗷,感谢~

ironboxer commented 3 years ago

你会了再看相当于复习了。 当前你不会的时候来看,还是不会。 这个更像是自己的笔记。

fuyu01 commented 3 years ago

有个问题请教一下,sync.Once加锁我能理解,但是为什么要用atomic原子操作呢,如果直接对done判断和赋值会有什么问题?

douglarek commented 3 years ago

有个问题请教一下,sync.Once加锁我能理解,但是为什么要用atomic原子操作呢,如果直接对done判断和赋值会有什么问题?

判断和赋值不是原子操作,有可能会有间隙存在两个 goroutine 同时改了。

fuyu01 commented 3 years ago

@douglarek

有个问题请教一下,sync.Once加锁我能理解,但是为什么要用atomic原子操作呢,如果直接对done判断和赋值会有什么问题?

判断和赋值不是原子操作,有可能会有间隙存在两个 goroutine 同时改了。

我看代码有个互斥锁,应该只有一个调用可以得到锁然后去修改那个值吧,不存在两个同时吧

douglarek commented 3 years ago

@douglarek

有个问题请教一下,sync.Once加锁我能理解,但是为什么要用atomic原子操作呢,如果直接对done判断和赋值会有什么问题?

判断和赋值不是原子操作,有可能会有间隙存在两个 goroutine 同时改了。

我看代码有个互斥锁,应该只有一个调用可以得到锁然后去修改那个值吧,不存在两个同时吧

嗯,但是 go race 机制不会识别它,所以你 go build -race 的时候会报错,我好像上面有提到:https://github.com/draveness/blog-comments/issues/151#issuecomment-732100774

fuyu01 commented 3 years ago

@douglarek

@douglarek

有个问题请教一下,sync.Once加锁我能理解,但是为什么要用atomic原子操作呢,如果直接对done判断和赋值会有什么问题?

判断和赋值不是原子操作,有可能会有间隙存在两个 goroutine 同时改了。

我看代码有个互斥锁,应该只有一个调用可以得到锁然后去修改那个值吧,不存在两个同时吧

嗯,但是 go race 机制不会识别它,所以你 go build -race 的时候会报错,我好像上面有提到:https://github.com/draveness/blog-comments/issues/151#issuecomment-732100774

懂了,再问下哈,race报错是不是会编译不过?如果能编译过直接跑会有问题么

douglarek commented 3 years ago

@douglarek

@douglarek

有个问题请教一下,sync.Once加锁我能理解,但是为什么要用atomic原子操作呢,如果直接对done判断和赋值会有什么问题?

判断和赋值不是原子操作,有可能会有间隙存在两个 goroutine 同时改了。

我看代码有个互斥锁,应该只有一个调用可以得到锁然后去修改那个值吧,不存在两个同时吧

嗯,但是 go race 机制不会识别它,所以你 go build -race 的时候会报错,我好像上面有提到:#151 (comment)

懂了,再问下哈,race报错是不是会编译不过?如果能编译过直接跑会有问题么

编译不加 -race 参数可以编译通过,但是直接跑可能会有问题

mohuishou commented 3 years ago

关于 WaitGroup 里面的 state1 字段的表述是不是有点问题,文章中说的是 64 位 counter 在前信号量在后,32 反过来。但是看注释和源码,是因为 64 位原子操作需要 64 位(8字节)对齐,但是 32 位的编辑器不能保证这一点,所以在 state 方法里面,是 8 字节对齐的是第一种方式,没有 8 字节对齐的是第二种方式。 这里表述的主要问题是 32 位应该不能直接说就一定不是对齐的吧?

mohuishou commented 3 years ago

还有一个问题,state1 这个图里面的 counter 和 waiter 是不是画反了

 // 在 waiter 上加上 delta 值
state := atomic.AddUint64(statep, uint64(delta)<<32)
// 取出当前的 counter
v := int32(state >> 32)
// 取出当前的 waiter,正在等待 goroutine 数量
w := uint32(state)
DustOak commented 3 years ago

@fuyu01

@douglarek

@douglarek

有个问题请教一下,sync.Once加锁我能理解,但是为什么要用atomic原子操作呢,如果直接对done判断和赋值会有什么问题?

判断和赋值不是原子操作,有可能会有间隙存在两个 goroutine 同时改了。

我看代码有个互斥锁,应该只有一个调用可以得到锁然后去修改那个值吧,不存在两个同时吧

嗯,但是 go race 机制不会识别它,所以你 go build -race 的时候会报错,我好像上面有提到:https://github.com/draveness/blog-comments/issues/151#issuecomment-732100774

懂了,再问下哈,race报错是不是会编译不过?如果能编译过直接跑会有问题么

如果你不用原子操作的话,互斥锁加锁只加在了

func (o *Once) doSlow(f func()) {
    o.m.Lock()
    defer o.m.Unlock()
    if o.done == 0 {
        defer atomic.StoreUint32(&o.done, 1)
        f()
    }
}

里面 此时如果对这个done进行非原子操作写的话

unc (o *Once) Do(f func()) {

    if atomic.LoadUint32(&o.done) == 0 {

        o.doSlow(f)
    }
}

这个读取done的时候如果和写同时发生,是会出现问题的,因为外层这块是没有互斥锁保护的

fuyu01 commented 3 years ago

@fuyu01

@douglarek

@douglarek

有个问题请教一下,sync.Once加锁我能理解,但是为什么要用atomic原子操作呢,如果直接对done判断和赋值会有什么问题?

判断和赋值不是原子操作,有可能会有间隙存在两个 goroutine 同时改了。

我看代码有个互斥锁,应该只有一个调用可以得到锁然后去修改那个值吧,不存在两个同时吧

嗯,但是 go race 机制不会识别它,所以你 go build -race 的时候会报错,我好像上面有提到:#151 (comment)

懂了,再问下哈,race报错是不是会编译不过?如果能编译过直接跑会有问题么

如果你不用原子操作的话,互斥锁加锁只加在了

func (o *Once) doSlow(f func()) {
  o.m.Lock()
  defer o.m.Unlock()
  if o.done == 0 {
      defer atomic.StoreUint32(&o.done, 1)
      f()
  }
}

里面 此时如果对这个done进行非原子操作写的话

unc (o *Once) Do(f func()) {

  if atomic.LoadUint32(&o.done) == 0 {

      o.doSlow(f)
  }
}

这个读取done的时候如果和写同时发生,是会出现问题的,因为外层这块是没有互斥锁保护的

是会出现同时读写o对象的done属性,但这个int类型的值同时读写会出现什么问题,怎么复现这种问题呢

laopoom commented 3 years ago

小白请教一下,我理解的原语是操作系统内核中特定程序段,执行时不被打断。文章里说go提供的原语,是不是应该区分于操作系统原语?

draveness commented 3 years ago

小白请教一下,我理解的原语是操作系统内核中特定程序段,执行时不被打断。文章里说go提供的原语,是不是应该区分于操作系统原语?

原语其实是 primitive 翻译过来的

dillanzhou commented 3 years ago

golang/sync/semaphore.Weighted.Acquire的示意代码里为啥特别把Lock省略了。。。看的时候纳闷了好一会,建议加上。

yuelog commented 3 years ago

确实第一次看会有些懵,比如”但是刚被唤起的 Goroutine 与新创建的 Goroutine 竞争时,大概率会获取不到锁“,第一次读以为是"被唤起的 Goroutine 与新创建的 Goroutine "获取不到锁,连接着后面的”它“的指代也理解错了。查了下相关资料才明白原来是指队列里的获取不到,确实如评论区某大佬所言,如果自己懂那就相当于复习,不懂的话有些地方确实一看有点糊涂

draveness commented 3 years ago

@yuelog 确实第一次看会有些懵,比如”但是刚被唤起的 Goroutine 与新创建的 Goroutine 竞争时,大概率会获取不到锁“,第一次读以为是"被唤起的 Goroutine 与新创建的 Goroutine "获取不到锁,连接着后面的”它“的指代也理解错了。查了下相关资料才明白原来是指队列里的获取不到,确实如评论区某大佬所言,如果自己懂那就相当于复习,不懂的话有些地方确实一看有点糊涂

你理解的『它』代指的是什么,如果确实不够清楚,我可以改一下

woofyzhao commented 3 years ago

建议Atomic也在开头简单提下,内容更完整

shenguanjiejie commented 3 years ago

勘误: "sync.Cond 能够让出处理器的使用权,提供 CPU 的利用率。" 这里的"提供"应该是"提高"吧?


2021-06-08 UPDATES: 已修复

callmePicacho commented 3 years ago

勘误:"了较为基础的同步功能,但是它们是一种相对原始的同步机制,在多数情况下,我们都应该使用抽象层级的更高的 Channel 实现同步。" 前一句"了较为基础的同步功能",这里读不通啊?


2021-07-01: UPDATES 已修复

cuglaiyp commented 3 years ago

请问一下sync.runtime_Semrelease参数handoff的true和false的区别是什么?看了一下源码没看太懂。

caoxingdong commented 3 years ago

"了较为基础的同步功能,但是它们是一种相对原始的同步机制,在多数情况下,我们都应该使用抽象层级的更高的 Channel 实现同步。" 这第一个分句是不是有笔误?

Dup4 commented 3 years ago

两次调用 sync.Once.Do 方法传入不同的函数只会执行第一次调传入的函数;

勘误:「第一次调传入的函数」感觉读不通,感觉「调」这个字多余了。

jackzhou121 commented 3 years ago

Cond中的wait设计,为啥不把lock直接放到函数里面,而是要放到wait前面调用,有点别扭啊

Hchenwy commented 2 years ago

有一点疑惑,为什么mutex 执行unlock的操作为什么需要用原子加,lock的实现已经保证并发安全,unlock直接赋值应该就可以了,不需要再锁一次总线,这样效率不会更高?

kklll commented 2 years ago

https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-sync-primitives/#mutex Mutex的介绍中 图 6-6 互斥锁的状态下面关于mutexWoken 的字段描述:” mutexWoken — 表示从正常模式被从唤醒; “ 应该改为 ”mutexWoken — 表示从正常模式被唤醒;“