31 | sync.WaitGroup和sync.Once
31 | sync.WaitGroup和sync.Once
讲述:黄洲君
时长13:35大小6.22M
前导内容:sync包的WaitGroup类型
问题解析
知识扩展
问题:sync.Once类型值的Do方法是怎么保证只执行参数函数一次的?
总结
思考题
赞 17
提建议
精选留言(40)
- liangjf2019-02-26“双重检查” 貌似也并不是完全安全的吧,像c++11那样加入内存屏障才是真正线性安全的。go有这类接口吗
作者回复: Go语言底层内置了内存屏障。它的好处就是不用像C++那样什么都需要自己搞。
共 2 条评论18 - 唐大少在路上。。。2019-10-24个人感觉Once里面的逻辑设计得不够简洁,既然目的就是只要能够拿到once的锁的gorountine就会消费掉这个once,那其实直接在Do方法的最开始用if atomic.CompareAndSwapUint32(&o.done, 0,1)不就行了,连锁都不用。 还请老师指正,哈哈
作者回复: 这样不行啊,它还得执行你给它的函数啊。怎么能在没执行函数之前就把 done 变成 1 呢,对吧。但如果是在执行之后 swap,那又太晚了,有可能出现重复执行函数的情况。 所以 Once 中才有两个执行路径,一个是仅包含原子操作的快路径,另一个是真正准备执行函数的慢路径。这样才可以兼顾多种情况,让总体性能更优。
共 3 条评论15 - ONLY2019-12-10可不可以把 sync.once 理解为单例模式,比如连接数据库只需要连接一次,把连接数据库的代码实在once.do()里面
作者回复: 它跟单例模式还不太一样。单例模式指的是某类结构的唯一实例,而 once 指的是对某段代码的唯一一次执行。它们的维度不一样。 连接数据库的代码其实不太适合放到 Do 里面执行,或者说不太恰当。初始化数据库链接的代码可以放到里面。而,断链重连的机制也应该在其中。
9 - 虢國技醬2019-11-26二刷走起6
- 超大叮当当2019-03-13sync.Once 不用 Mutex ,直接用 atomic.CompareAndSwapUint32 函数也可以安全吧?
作者回复: 原子操作是CPU级别的互斥,而且防中断。但是支持的数据类型很少,而且并不灵活。所以如果是对代码块进行保护,还需要用锁。
6 - moonfox2021-05-09请问一下,在 sync.Once的源码里, doSlow()方法中,已经用了o.m.Lock(),什么写入o.done=1的时候,还要用原子写入呢?
作者回复: 这是两码事啊,原子操作还有一个作用是保证被操作值的完整性。比如,done字段的值要么是0要么是1。别忘了,done字段的值是由32个比特位组成的。如果在修改值的过程中(还没改完),其他的代码在读取它,那岂不是会读到一个非0非1的值吗?(这是一个小概率问题,但是万一出了错,复现都没法复现,排查起来就太困难了,所以恰恰需要极力避免) 就像源码中的 if atomic.LoadUint32(&o.done) == 0 { 这里。 这行代码可没有锁的加持啊,它可不管另一个goroutine执行到doSlow函数中的哪一步了。另外,对一个值的原子操作必须全面覆盖(如果用,就都要用原子操作)。
6 - 蔺晨2018-11-21思考题 : func getAllGoroutineResult(){ wg := sync.WaitGroup{} wg.Add(3) once := sync.Once{} var aAndb int var aStrAndb string var gflag int32 addNum := func(a,b int, ret *int) { defer wg.Done() time.Sleep(time.Millisecond * 2000) *ret = a+b atomic.AddInt32(&gflag,1) } addStr := func(a,b string, ret *string) { defer wg.Done() time.Sleep(time.Millisecond * 1000) *ret = a+b atomic.AddInt32(&gflag,1) } // waitRet需要等待 addNum和addStr执行完成后的结果 waitRet := func(ret *int, strRet *string) { defer wg.Done() once.Do(func() { for atomic.LoadInt32(&gflag) != 2 { fmt.Println("Wait: addNum & addStr") time.Sleep(time.Millisecond * 200) } }) fmt.Println(fmt.Sprintf("AddNum's Ret is: %d\n", *ret)) fmt.Println(fmt.Sprintf("AddStr's Ret is: %s\n", *strRet)) } // waitRet goroutine等待AddNum和AddStr结束 go waitRet(&aAndb, &aStrAndb) go addNum(10, 20, &aAndb) go addStr("测试结果", "满意不?", &aStrAndb) wg.Wait() }展开4
- 窗外2019-11-03go func () { wg.Done() fmt.Println("send complete") }() 老师,为什么在Done()后的代码就不会被执行呢?
作者回复: 你在后面 wg.Wait() 了吗?
共 2 条评论3 - ricktian2018-11-30执行结果如果不用channel实现,还有什么方法?请老师指点~共 2 条评论3
- Laughing2018-10-30子任务的结果应该用通道来传递吧。另外once的应用场景还是没有理解。郝大能简单说一下么?
作者回复: 可以通过通道,但这就不是wg的作用范围了。once一般是执行只应该执行一次的任务,比如初始化连接池等等。你可以在go源码里搜一下,用的地方还是不少的。
3 - undifined2018-10-22执行结果用 Callback,放在通道中,在主 goroutine 中接收返回结果3
- 罗峰2021-01-22老师,你好,waitgroup的计数周期这个概念是自创的吗?使用上感觉 只要 add操作在wait语句之前执行就可以,使用个例子: for { select { case <- cancel: break; case <- taskqueue: go func { wg.add(1) .... defer wg.done() } } } wg.wait()展开
作者回复: WaitGroup与操作系统的信号灯异曲同工。 必须要有 wg.Done() 啊,否则计数就无法归零,wg.Wait() 也就无法消除阻塞。
2 - 12872020-05-25没理解使用once和自己只调用一次有什么区别,类似初始化的操作,我在程序执行前写个init也是只执行一次吧,求教
作者回复: 同一个 sync.Once 实例的 Do 方法只会被有效调用一次。init 函数是在当前程序中只会被调用一次,而且它的作用域是代码包。
共 2 条评论3 - 手指饼干2019-10-10请问老师,如下deferFunc为什么要用func包装起来,直接使用defer deferFunc()不可以吗? func addNum(numP *int32, id, max int32, deferFunc func()) { defer func() { deferFunc() }() //... }展开
作者回复: 你那么写也可以,我弄一坨只是想引起你们的注意。在不影响程序功能和运行效率的前提下,我会在程序里尽量多展示几种写法。
共 2 条评论3 - 传说中的成大大2020-04-10今日总结 这里主要讲了sync/waitgroup和sync/once这两种同步的方式 waitgroup一般用于一对多的同步 例如 主goroutine等待其他goroutine的执行完成 唯一要注意的是 add方法和wait方法不要并行执行 也即 wait方法调用过后 不要立即再调用add方法 once值 主要还是保证函数只被执行一次 且只有第一次调用Do方法的参数函数会被执行一次 once值底层依赖的是互斥锁 所以操作和互斥锁很类似 并且 如果某个Do的参数函数一直不结束 那么其他调用Do方法的goroutine都会被阻塞在获取互斥锁这一步,只有当首次Do方法的参数函数执行结束过后 这些goroutine才会被逐一唤醒 所以要注意 Once 在使用死的死锁展开1
- bluuus2019-09-06func coordinateWithWaitGroup() { var wg sync.WaitGroup wg.Add(2) num := int32(0) fmt.Printf("The number: %d [with sync.WaitGroup]\n", num) max := int32(10) go addNum(&num, 3, max, wg.Done) //go addNum(&num, 4, max, wg.Done) wg.Wait() } run result: The number: 0 [with sync.WaitGroup] The number: 2 [3-0] The number: 4 [3-1] The number: 6 [3-2] The number: 8 [3-3] The number: 10 [3-4] fatal error: all goroutines are asleep - deadlock! 执行这段代码会死锁,我以为最多在wai()方法那儿阻塞,谁能解释一下?展开
作者回复: 你对 addNum 函数有改动吗?
共 3 条评论1 - MClink2022-07-10easy case func TestWaitGroup() { var wg sync.WaitGroup wg.Add(3) for i := 0; i < 3; i++ { go func() { defer wg.Done() time.Sleep(time.Second * 3) fmt.Println("done!") }() } wg.Wait() fmt.Println("over") } func TestOnce() { var o sync.Once var wg sync.WaitGroup wg.Add(10) for i := 0; i < 10; i++ { go func() { defer wg.Done() time.Sleep(time.Second * 1) o.Do(func() { fmt.Println("mclink") }) }() } wg.Wait() fmt.Println("over") }展开
- NeoMa2022-01-17您好,关于once.go中 doSlow方法有两个疑问: 1. 在拿到m.Lock()锁之后的o.done == 0 的判断能否省略?或者说这个判断针对的是某些可能的未知场景吗?我的理解是,理论上此时这个值一定是0,但为了确保预防未知场景又做了一次判断。 2. defer atomic.StoreUint32(&o.done, 1) 这个操作是否可以不用原子操作,只是使用例如 o.done = 1类似的赋值操作? 希望您多多指正。展开
作者回复: 1. 当然不能省略了,如果在当前 goroutine 的 atomic.LoadUint32(&o.done) == 0 和 o.doSlow(f) 的执行间隙有其他 goroutine 执行到了 atomic.StoreUint32(&o.done, 1) ,那么再等到当前 goroutine 继续执行的时候 done 可就不是 0 了。 注意!锁只保证:它保护的某一个临界区内的代码在同一时刻只被某一个 goroutine 执行(也可以说接触到),而不会保证其中代码执行的原子性。所以,在其中代码执行期间也有可能被中断,中断的粒度通常是语句级别的。 2. 针对同一个共享变量的原子操作必须是完全的,否则就跟没做原子操作没啥区别。就像刚才说的,锁只负责保证串行访问和执行,它没法实现原子性操作。在这里,锁和原子操作是各司其职的。(除非你完全用锁把对 done 的一切操作都保护起来,否则,即然用了原子操作来保证 done 的原子性就要用完全)
- 窝窝头2022-01-06执行结果可以通过共享内存、channel之类的,如果只是想让其他协程等待所有协程都完成后统一退出的话是不是每个协程里面wg.Done,然后wg.Wait就行了
作者回复: 对的
- Harlan2021-09-15go 里面使用waitGroup后,对多个goroutine 的结果还是得用 ch来处理,比较难受