面试官:实现协程同步有哪些方式?

为什么要做同步

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

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

举个栗子:

package main

import(
    "fmt"
    "time"
)

var share_cnt uint64 = 0

func 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.go
1014184
dashu@dashu > /data1/htdocs/go_practice > go run test.go
1026029
dashu@dashu > /data1/htdocs/go_practice > go run test.go
19630
...

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

协程同步方法

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

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)
}

打印输出:

goroutine1
goroutine2

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

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

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

channel(信道)

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

package main

import (
    "fmt"
    "time"
)

func producer(ch chan int, count int) {
    for i := 1; i <= count; i++ {
        fmt.Println("大妈做第", i, "个面包")
        ch <- i
        
        // 睡眠一下,可以让整个生产消费看得更清晰点
        time.Sleep(time.Second * time.Duration(1))
    }
}

func consumer(ch chan int, count int) {
    for v := range ch {
        fmt.Println("大叔吃了第", v, "个面包")
        count--
        if count == 0 {
            fmt.Println("没面包了,大叔也饱了")
            close(ch)
        }
    }
}

func main() {
    ch := make(chan int)
    count := 5
    go producer(ch, count)
    consumer(ch, count)
}

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

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

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

「Tip」

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

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

package main

import (
    "fmt"
    "time"
)

func producer(ch chan int, count int) {
    for i := 1; i <= count; i++ {
        fmt.Println("大妈做第", i, "个面包")
        ch <- i
        
        // 睡眠一下,可以让整个生产消费看得更清晰点
        time.Sleep(time.Second * time.Duration(1))
    }
}

func consumer(ch chan int, count int) {
    for v := range ch {
        fmt.Println("大叔吃了第", v, "个面包")
        count--
        if count == 0 {
            fmt.Println("没面包了,大叔也饱了")
        }
    }
}

func main() {
    ch := make(chan int)
    count := 5
    go producer(ch, count)
    consumer(ch, count)
    close(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 +0x5f
main.main()
 /data1/htdocs/go_project/src/github.com/cnyygj/go_practice/test.go:32 +0x7c
exit status 2

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

sync.WaitGroup

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

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

还是看栗子:

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 1
Goroutine 2

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

面试官:实现协程同步有哪些方式?

展开阅读全文

页面更新:2024-04-26

标签:死锁   个协   大妈   大叔   面包   睡眠   逻辑   内存   位置   原因   结束   代码   操作   方式   方法   数据   科技

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top