go 怎么等待所有的协程完成_面试官:实现协程同步有哪些方式?

本文探讨了为何需要进行同步处理,通过示例说明了睡眠、通道(channel)和sync.WaitGroup在解决goroutine间数据同步问题中的作用。通过大妈和大叔的面包生产与消费例子,解释了channel如何确保数据一致性,以及WaitGroup如何通过计数管理协程完成。
摘要由CSDN通过智能技术生成

为什么要做同步

在进入正题前,我们先习惯性地摸着良心问问自(ji)己 (ji) :为什么要做同步处理?

假设现在有多个协程并发访问操作同一块内存中的数据,那么可能上一纳秒第一个协程刚把数据从寄存器拷贝到内存,第二个协程马上又把此数据用它修改的值给覆盖了,这样共享数据变量会乱套。

举个栗子:

package mainimport(    "fmt"    "time")var share_cnt uint64 = 0func incrShareCnt() {    for i:=0; i < 10000; i++ {        share_cnt++    }}func main()  {    for i:=0; i < 2; i++ {        go incrShareCnt()    }    time.Sleep(10*time.Second)        fmt.Println(share_cnt)}

上面代码用2个协程序并发各自增一个全局变量1000000 次,我们来看一下打印输出的结果:

dashu@dashu > /data1/htdocs/go_practice > go run test.go1014184dashu@dashu > /data1/htdocs/go_practice > go run test.go1026029dashu@dashu > /data1/htdocs/go_practice > go run test.go19630...

从打印结果我们可以看到,虽然代码中我们对一个全局变量自增了20000次,但是没有一次打印输出20000的结果,原因就是因为协程间共享数据时发生了数据覆盖。实际上面的代码无聊sleep多就久都不会打印输出20000。

协程同步方法

那么,如何才能让数据在goroutine之间达到同步呢?下面跟大家分享以下三种数据同步的方式:

  • time.Sleep
  • channel
  • sync.WaitGroup

time.Sleep

为什么sleep可以用来实现数据同步呢?我们看个栗子:

func main()  {    go func() {        fmt.Println("goroutine1")    }()    go func() {        fmt.Println("goroutine2")    }()}

执行上面那段代码你会发现没有任何输出,原因是:主协程在两个协程还没执行完就已经结束了,而主协程结束时会结束所有其他协程, 所以导致代码运行的结果什么都没有。

我们在主协程结束前 sleep 一段时间就 可能出现 了结果:

func main()  {    go func() {        fmt.Println("goroutine1")    }()    go func() {        fmt.Println("goroutine2")    }()    time.Sleep(time.Second)}

打印输出:

goroutine1goroutine2

为什么上面我要说 “可能会出现” 呢?上面代码中我们设置了睡眠时间为1s,由于协程的处理逻辑比较简单,所以能正常打印输出上面结果;如果我这两个协程里面执行了很复杂的逻辑操作(时间大于 1s),那么就会发现依旧也是无结果打印出来的。

所以又一个问题来了:我们无法确定需要睡眠多久

看来这sleep着实不靠谱,有没有什么办法来代替sleep呢?答案肯定是有的,我们来看第二种方法。

channel(信道)

channel是如何实现goroutine同步的呢?我们再看个典型的栗子:channel实现简单的生产者和消费者

package mainimport (    "fmt"    "time")func producer(ch chan int, count int) {    for i := 1; i <= count; i++ {        fmt.Println("大妈做第", i, "个面包")        ch 

上面代码中,我们另外起了个 goroutine 让大妈来生产5个面包(实际就是往channel中写数据),主 goroutine 让大叔不断吃面包(从channel中读数据)。我们来看一下输出结果:

大妈做第 1 个面包大叔吃了第 1 个面包大妈做第 2 个面包大叔吃了第 2 个面包大妈做第 3 个面包大叔吃了第 3 个面包大妈做第 4 个面包大叔吃了第 4 个面包大妈做第 5 个面包大叔吃了第 5 个面包没面包了,大叔也饱了

从输出结果我们可以看到,大妈一共做了5个面包,大叔一共吃了5个面包,同步上了!

「Tip」

上面代码,我们用 for-range 来读取 channel的数据,for-range 是一个很有特色的语句,有以下特点:

  • 如果 channel 已经被关闭,它还是会继续执行,直到所有值被取完,然后退出执行
  • 如果通道没有关闭,但是channel没有可读取的数据,它则会阻塞在 range 这句位置,直到被唤醒。
  • 如果 channel 是 nil,那么同样符合我们上面说的的原则,读取会被阻塞,也就是会一直阻塞在 range 位置。

我们来验证一下,我们把上面代码中的 close(ch) 移到主协程中试试:

package mainimport (    "fmt"    "time")func producer(ch chan int, count int) {    for i := 1; i <= count; i++ {        fmt.Println("大妈做第", i, "个面包")        ch 

打印输出:

大妈做第 1 个面包大叔吃了第 1 个面包大妈做第 2 个面包大叔吃了第 2 个面包大妈做第 3 个面包大叔吃了第 3 个面包大妈做第 4 个面包大叔吃了第 4 个面包大妈做第 5 个面包大叔吃了第 5 个面包没面包了,大叔也饱了fatal error: all goroutines are asleep - deadlock!goroutine 1 [chan receive]:main.consumer(0xc00008c060, 0x0) /data1/htdocs/go_project/src/github.com/cnyygj/go_practice/test.go:19 +0x5fmain.main() /data1/htdocs/go_project/src/github.com/cnyygj/go_practice/test.go:32 +0x7cexit status 2

果然阻塞掉了,最终形成了死锁,抛出异常了。

sync.WaitGroup

如果你觉的上面两种方法还不过瘾,接下来我们再看个方法:sync.WaitGroup

WaitGroup 内部实现了一个计数器,用来记录未完成的操作个数,它提供了三个方法:

  • Add() 用来添加计数
  • Done() 用来在操作结束时调用,使计数减一 【我不会告诉你 Done() 方法的实现其实就是调用 Add(-1)】
  • Wait() 用来等待所有的操作结束,即计数变为 0,该函数会在计数不为 0 时等待,在计数为 0 时立即返回

还是看栗子:

func main()  {    var wg sync.WaitGroup    wg.Add(2) // 因为有两个动作,所以增加2个计数    go func() {        fmt.Println("Goroutine 1")        wg.Done() // 操作完成,减少一个计数    }()    go func() {        fmt.Println("Goroutine 2")        wg.Done() // 操作完成,减少一个计数    }()    wg.Wait() // 等待,直到计数为0}

打印输出:

Goroutine 1Goroutine 2

以上就是今天要跟大家分享的内容,欢迎留言交流~

59c1b5b1b911bdbfc333a74eef1debf3.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值