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

为什么要做同步在进入正题前 , 我们先习惯性地摸着良心问问自(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 <- 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 := 5go 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 是一个很有特色的语句 , 有以下特点:
  • 如果 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 <- 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 := 5go producer(ch, count)consumer(ch, count)close(ch)}