为什么要做同步
在进入正题前,我们先习惯性地摸着良心问问自(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](https://i-blog.csdnimg.cn/blog_migrate/7a94d8e6dbeb1d3a8318ae829f4557a7.jpeg)