在进入正题前,我们先习惯性地摸着良心问问自(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之间达到同步呢?下面跟大家分享以下三种数据同步的方式:
为什么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是如何实现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
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
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号