Go 语言核心 36 讲
郝林
《Go 并发编程实战》作者,前轻松筹大数据负责人
79610 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 55 讲
Go 语言核心 36 讲
15
15
1.0x
00:00/00:00
登录|注册

31 | sync.WaitGroup和sync.Once

思考题
特点
done字段
Do方法
异常情况
使用禁忌
计数周期
计数器值小于0的问题
coordinateWithWaitGroup函数示例
Wait方法
Done方法
Add方法
WaitGroup类型介绍
coordinateWithChan函数示例
主goroutine等待其他goroutine运行结束
声明通道容量与goroutine数量相同
sync.Once
sync.WaitGroup
通道
sync.WaitGroup和sync.Once
参考文章

该思维导图由 AI 生成,仅供参考

我们在前几次讲的互斥锁、条件变量和原子操作都是最基本重要的同步工具。在 Go 语言中,除了通道之外,它们也算是最为常用的并发安全工具了。
说到通道,不知道你想过没有,之前在一些场合下里,我们使用通道的方式看起来都似乎有些蹩脚。
比如:声明一个通道,使它的容量与我们手动启用的 goroutine 的数量相同,之后再利用这个通道,让主 goroutine 等待其他 goroutine 的运行结束。
这一步更具体地说就是:让其他的 goroutine 在运行结束之前,都向这个通道发送一个元素值,并且,让主 goroutine 在最后从这个通道中接收元素值,接收的次数需要与其他的 goroutine 的数量相同。
这就是下面的coordinateWithChan函数展示的多 goroutine 协作流程。
func coordinateWithChan() {
sign := make(chan struct{}, 2)
num := int32(0)
fmt.Printf("The number: %d [with chan struct{}]\n", num)
max := int32(10)
go addNum(&num, 1, max, func() {
sign <- struct{}{}
})
go addNum(&num, 2, max, func() {
sign <- struct{}{}
})
<-sign
<-sign
}
其中的addNum函数的声明在 demo65.go 文件中。addNum函数会把它接受的最后一个参数值作为其中的defer函数。
我手动启用的两个 goroutine 都会调用addNum函数,而它们传给该函数的最后一个参数值(也就是那个既无参数声明,也无结果声明的函数)都只会做一件事情,那就是向通道sign发送一个元素值。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

`sync`包中的`WaitGroup`类型和`Once`类型都是Go语言中重要的同步工具。`WaitGroup`类型适用于实现一对多的goroutine协作流程,而`Once`类型则保证传入的参数函数只执行一次。`WaitGroup`类型使用原子操作来管理计数器,需要注意避免计数器值小于0或在调用`Wait`方法的同时并发增加计数器值。相比之下,`Once`类型更简单,只有一个`Do`方法,保证参数函数只执行一次,但可能导致阻塞。两者都是高层次的同步工具,基于基本的通用工具实现特定功能。读者在使用`WaitGroup`值实现一对多的goroutine协作流程时,需要考虑如何让分发子任务的goroutine获得各个子任务的具体执行结果。这篇文章深入解析了`WaitGroup`和`Once`类型的使用方式和特点,适合对并发编程感兴趣的读者阅读学习。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Go 语言核心 36 讲》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(41)

  • 最新
  • 精选
  • liangjf
    “双重检查” 貌似也并不是完全安全的吧,像c++11那样加入内存屏障才是真正线性安全的。go有这类接口吗

    作者回复: Go语言底层内置了内存屏障。它的好处就是不用像C++那样什么都需要自己搞。

    2019-02-26
    2
    20
  • 唐大少在路上。。。
    个人感觉Once里面的逻辑设计得不够简洁,既然目的就是只要能够拿到once的锁的gorountine就会消费掉这个once,那其实直接在Do方法的最开始用if atomic.CompareAndSwapUint32(&o.done, 0,1)不就行了,连锁都不用。 还请老师指正,哈哈

    作者回复: 这样不行啊,它还得执行你给它的函数啊。怎么能在没执行函数之前就把 done 变成 1 呢,对吧。但如果是在执行之后 swap,那又太晚了,有可能出现重复执行函数的情况。 所以 Once 中才有两个执行路径,一个是仅包含原子操作的快路径,另一个是真正准备执行函数的慢路径。这样才可以兼顾多种情况,让总体性能更优。

    2019-10-24
    3
    17
  • only
    可不可以把 sync.once 理解为单例模式,比如连接数据库只需要连接一次,把连接数据库的代码实在once.do()里面

    作者回复: 它跟单例模式还不太一样。单例模式指的是某类结构的唯一实例,而 once 指的是对某段代码的唯一一次执行。它们的维度不一样。 连接数据库的代码其实不太适合放到 Do 里面执行,或者说不太恰当。初始化数据库链接的代码可以放到里面。而,断链重连的机制也应该在其中。

    2019-12-10
    10
  • moonfox
    请问一下,在 sync.Once的源码里, doSlow()方法中,已经用了o.m.Lock(),什么写入o.done=1的时候,还要用原子写入呢?

    作者回复: 这是两码事啊,原子操作还有一个作用是保证被操作值的完整性。比如,done字段的值要么是0要么是1。别忘了,done字段的值是由32个比特位组成的。如果在修改值的过程中(还没改完),其他的代码在读取它,那岂不是会读到一个非0非1的值吗?(这是一个小概率问题,但是万一出了错,复现都没法复现,排查起来就太困难了,所以恰恰需要极力避免) 就像源码中的 if atomic.LoadUint32(&o.done) == 0 { 这里。 这行代码可没有锁的加持啊,它可不管另一个goroutine执行到doSlow函数中的哪一步了。另外,对一个值的原子操作必须全面覆盖(如果用,就都要用原子操作)。

    2021-05-09
    9
  • 超大叮当当
    sync.Once 不用 Mutex ,直接用 atomic.CompareAndSwapUint32 函数也可以安全吧?

    作者回复: 原子操作是CPU级别的互斥,而且防中断。但是支持的数据类型很少,而且并不灵活。所以如果是对代码块进行保护,还需要用锁。

    2019-03-13
    6
  • 1287
    没理解使用once和自己只调用一次有什么区别,类似初始化的操作,我在程序执行前写个init也是只执行一次吧,求教

    作者回复: 同一个 sync.Once 实例的 Do 方法只会被有效调用一次。init 函数是在当前程序中只会被调用一次,而且它的作用域是代码包。

    2020-05-25
    2
    3
  • 窗外
    go func () { wg.Done() fmt.Println("send complete") }() 老师,为什么在Done()后的代码就不会被执行呢?

    作者回复: 你在后面 wg.Wait() 了吗?

    2019-11-03
    2
    3
  • 手指饼干
    请问老师,如下deferFunc为什么要用func包装起来,直接使用defer deferFunc()不可以吗? func addNum(numP *int32, id, max int32, deferFunc func()) { defer func() { deferFunc() }() //... }

    作者回复: 你那么写也可以,我弄一坨只是想引起你们的注意。在不影响程序功能和运行效率的前提下,我会在程序里尽量多展示几种写法。

    2019-10-10
    2
    3
  • Laughing
    子任务的结果应该用通道来传递吧。另外once的应用场景还是没有理解。郝大能简单说一下么?

    作者回复: 可以通过通道,但这就不是wg的作用范围了。once一般是执行只应该执行一次的任务,比如初始化连接池等等。你可以在go源码里搜一下,用的地方还是不少的。

    2018-10-30
    3
  • 罗峰
    老师,你好,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() 也就无法消除阻塞。

    2021-01-22
    2
收起评论
显示
设置
留言
41
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部