Go语言核心36讲
郝林
《Go并发编程实战》作者,前轻松筹大数据负责人
立即订阅
24066 人已学习
课程目录
已完结 54 讲
0/4登录后,你可以任选4讲全文学习。
开篇词+学习路线 (3讲)
开篇词 | 跟着学,你也能成为Go语言高手
免费
预习篇 | 写给0基础入门的Go语言学习者
50 | 学习专栏的正确姿势
模块一:Go语言基础知识 (6讲)
01 | 工作区和GOPATH
02 | 命令源码文件
03 | 库源码文件
04 | 程序实体的那些事儿(上)
05 | 程序实体的那些事儿(中)
06 | 程序实体的那些事儿 (下)
模块二:Go语言进阶技术 (16讲)
07 | 数组和切片
08 | container包中的那些容器
09 | 字典的操作和约束
10 | 通道的基本操作
11 | 通道的高级玩法
12 | 使用函数的正确姿势
13 | 结构体及其方法的使用法门
14 | 接口类型的合理运用
15 | 关于指针的有限操作
16 | go语句及其执行规则(上)
17 | go语句及其执行规则(下)
18 | if语句、for语句和switch语句
19 | 错误处理(上)
20 | 错误处理 (下)
21 | panic函数、recover函数以及defer语句 (上)
22 | panic函数、recover函数以及defer语句(下)
模块三:Go语言实战与应用 (27讲)
23 | 测试的基本规则和流程 (上)
24 | 测试的基本规则和流程(下)
25 | 更多的测试手法
26 | sync.Mutex与sync.RWMutex
27 | 条件变量sync.Cond (上)
28 | 条件变量sync.Cond (下)
29 | 原子操作(上)
30 | 原子操作(下)
31 | sync.WaitGroup和sync.Once
32 | context.Context类型
33 | 临时对象池sync.Pool
34 | 并发安全字典sync.Map (上)
35 | 并发安全字典sync.Map (下)
36 | unicode与字符编码
37 | strings包与字符串操作
38 | bytes包与字节串操作(上)
39 | bytes包与字节串操作(下)
40 | io包中的接口和工具 (上)
41 | io包中的接口和工具 (下)
42 | bufio包中的数据类型 (上)
43 | bufio包中的数据类型(下)
44 | 使用os包中的API (上)
45 | 使用os包中的API (下)
46 | 访问网络服务
47 | 基于HTTP协议的网络服务
48 | 程序性能分析基础(上)
49 | 程序性能分析基础(下)
尾声与思考题答案 (2讲)
尾声 | 愿你披荆斩棘,所向无敌
新年彩蛋 | 完整版思考题答案
Go语言核心36讲
登录|注册

31 | sync.WaitGroup和sync.Once

郝林 2018-10-22
我们在前几次讲的互斥锁、条件变量和原子操作都是最基本重要的同步工具。在 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/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《Go语言核心36讲》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(21)

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

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

    2019-02-26
    5
  • 蔺晨
    思考题 :
    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()
    }
    2018-11-21
    3
  • 唐大少在路上。。。
    个人感觉Once里面的逻辑设计得不够简洁,既然目的就是只要能够拿到once的锁的gorountine就会消费掉这个once,那其实直接在Do方法的最开始用if atomic.CompareAndSwapUint32(&o.done, 0,1)不就行了,连锁都不用。
    还请老师指正,哈哈

    作者回复: 这样不行啊,它还得执行你给它的函数啊。怎么能在没执行函数之前就把 done 变成 1 呢,对吧。但如果是在执行之后 swap,那又太晚了,有可能出现重复执行函数的情况。

    所以 Once 中才有两个执行路径,一个是仅包含原子操作的快路径,另一个是真正准备执行函数的慢路径。这样才可以兼顾多种情况,让总体性能更优。

    2019-10-24
    2
  • ricktian
    执行结果如果不用channel实现,还有什么方法?请老师指点~
    2018-11-30
    1
    2
  • 手指饼干
    请问老师,如下deferFunc为什么要用func包装起来,直接使用defer deferFunc()不可以吗?
    func addNum(numP *int32, id, max int32, deferFunc func()) {
    defer func() {
    deferFunc()
    }()
    //...
    }

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

    2019-10-10
    1
  • 超大叮当当
    sync.Once 不用 Mutex ,直接用 atomic.CompareAndSwapUint32 函数也可以安全吧?

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

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

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

    2018-10-30
    1
  • undifined
    执行结果用 Callback,放在通道中,在主 goroutine 中接收返回结果
    2018-10-22
    1
  • 虢国技匠
    二刷走起
    2019-11-26
  • 窗外
    go func () {
    wg.Done()
    fmt.Println("send complete")
    }()
    老师,为什么在Done()后的代码就不会被执行呢?

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

    2019-11-03
  • nora
    package main

    import (
    "fmt"
    "sync"
    )

    //不要把增加其计数器值的操作和调用其Wait方法的代码,放在不同的 goroutine 中执行
    //要杜绝对同一个WaitGroup值的两种操作的并发执行

    //使用WaitGroup值标准方式: 先统一Add,再并发Done,最后Wait

    func main() {
    var wg sync.WaitGroup
    i := 0
    for ; i < 2; i++ {
    wg.Add(1)
    go func(i int) {
    fmt.Println("i = ", i)
    defer wg.Done()
    }(i)
    }
    wg.Wait()
    return
    }
    2019-10-15
  • bluuus
    func 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 函数有改动吗?

    2019-09-06
    1
  • magina
    WaitGroup能不能设置超时

    作者回复: 本身不能,但是这很容易通过 select 语句和 time.Timer 解决。

    2019-08-05
  • 虢国技匠
    不用chan的情况下,
    传递一个指针变量到go执行的函数中,这样就可以在主goroutine中取到
    2019-07-26
  • M
    看了一下源码,Once是先将done值置为1后再执行的参数函数。所以应该不会阻塞等待函数执行的情况。
            if atomic.LoadUint32(&o.done) == 1 {
    return
    }
    // Slow-path.
    o.m.Lock()
    defer o.m.Unlock()
    if o.done == 0 {
    defer atomic.StoreUint32(&o.done, 1)
    f()
    }
    2019-01-31
    2
  • jacke
    问下为什么:Add方法和Wait方法的调用是在两个goroutine里面同时调用,Add会panic?
    wait不是在计数器为零的时候什么都不做吗?Add先执行后执行没问题啊
    2018-12-16
    1
  • 蔺晨
    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()
    }
    2018-11-21
  • Leon📷
    通过wait阻塞的协程的函数的参数传入指针,然后等wait()执行结束后,通过对应变量来收取值
    var wg sync.WaitGroup
    var a int
    wg.add(1)
    go func(a *int) {
      *a= 3
    }
    wg.Wait()
    使用a获取值
    是这样吗,老师
    2018-11-20
  • xian
    1. 可以使用通道来传递
    2. 在主协程中事先申请足够大的数组,按顺序来存储每个子协程的返回结果
    2018-11-04
  • zs阿帅
    受教了!
    2018-10-24
收起评论
21
返回
顶部