Golang并发协程同步WaitGroup | 青训营笔记

222 阅读5分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第3篇笔记

😆在这里,我对今天的课所新学到的Golang并发协程同步中的WaitGroup做了一次总结

😜Golang的其他知识在哪里找呢,那你就问对了

👨‍💻Golang基础复习 - 掘金 (juejin.cn) 在这里我总结了一些这篇文章没有提到的一些知识

😊如果有小伙伴能想到更多知识,欢迎大家在评论区留言,那么我们就开始吧

👩‍💻👨‍💻哟西,一个棕~

😎😎😎我是小小分割线

并发编程

协程

协程不是系统级线程,很多时候协程被称为“轻量级线程”、“微线程”、“纤程(fiber)”等。

简单来说可以认为协程是线程里不同的函数,这些函数之间可以相互快速切换

Golang中创建一个协程是非常简单的,如下,在函数前加一个go关键字就能做到

go task()

通道channel

在Golang中,如果各个协程内部需要交互,传输通信,那我们该这么做到呢

Golang此时是提供了一种称为channel的机制,用于在goroutine之间共享数据

如果我们需要在goroutine之间共享数据,channel可以充当goroutine间的中间人并提供一种机制来保证同步交换

在声明通道时,我们需要给定数据类型,此时我们就可以使用这个通道了

我们goroutine的数据的交互都可以使用这个通道

在任一时间点,任一通道只能有一个数据可以访问数据或传输,因此按照设计不会发生管道内的数据竞争

根据数据交互的方式,我们可以分为:

  1. 有缓冲的通道
  2. 没有缓冲的通道

我们来看一下两者的不同

有缓冲的通道,发送方无需等待接收方把管道内的资源拿走才能放置,也即是可以做到异步通信

而无缓冲通道相反,如果接收方不接收,那么会阻塞,也即是可以做到同步通信

但是其可以保证发送和接收的瞬间执行两个goroutine之间的交互,而缓冲通道做不到这样的保证

那我们的语法是怎样的呢

我们先来看看创建channel的语法

我们需要使用到make内置函数和chan这个通道的关键字


// 都需要指定数据的类型
channel := make(chan int)

// 可以通过声明第二个参数来创造带有缓冲的通道
channelWithBuffer := make(chan int, 2)

此时我们创建了channel,那我们该怎么去使用他们呢

Golang提供了<-运算符

func main() {

   // 创建了一个int类型的无缓冲的channel
   channel := make(chan int)

   for {
      go send(channel)

      receive(channel)
   }
}

// 发送
func send(channel chan int) {
   // 语法为:管道<-管道类型的值
   channel <- 5
}

// 接收
func receive(channel chan int) {
   // 关闭管道
   defer close(channel)
   
   // 语法为:<-管道
   fmt.Println(<-channel)
}

/* 
    打印为:
    5
    0
    panic: close of closed channel
*/

我们来了解一些Golang中channel的一些特性:

  1. 对于同一个channel,发送操作是互斥的,接收操作之间也是互斥的
  2. 发送和接收对元素的值的处理都是不可分割的
  3. 在完成发送前会被阻塞,接收也同样

我们来用代码看一下这些特性

func main() {

   channel := make(chan int)

   go send(channel)

   num := <-channel

   fmt.Println(num)
}

func send(channel chan int) {
   // 此时等待3秒
   // 接收方会被阻塞
   time.Sleep(time.Second * 3)
   channel <- 5
}

😎😎😎是我是我,我是小小分割线

协程同步

我们首先来看一下协程同步是什么

同步呢也就是两个协程之间会互相等待

欸?那我们刚刚不是说到不带缓冲的管道channel就是同步的吗

没错,无缓冲的channel就能做到协程同步,而且上面的代码实现也证明,在发送方完成发送前会一直阻塞

而且有缓冲的channel在缓冲区满了也可以做到协程同步

如果我们不需要channel传递数据,在其他的任务中需要协程同步,我们改怎么办呢?

我们还有什么方式能够做到协程同步呢?

sync包

在sync这个同步包中Golang官方提供了很多的同步的机制

就例如WaitGroup

WaitGroup

那么WaitGroup是什么呢?

英语翻译过来就是一个等待的一个小组

我们可以看一下他的源码的注释

// A WaitGroup waits for a collection of goroutines to finish.
// The main goroutine calls Add to set the number of
// goroutines to wait for. Then each of the goroutines
// runs and calls Done when finished. At the same time,
// Wait can be used to block until all goroutines have finished.
//
// A WaitGroup must not be copied after first use.
type WaitGroup struct {
   noCopy noCopy

   // 64-bit value: high 32 bits are counter, low 32 bits are waiter count.
   // 64-bit atomic operations require 64-bit alignment, but 32-bit
   // compilers only guarantee that 64-bit fields are 32-bit aligned.
   // For this reason on 32 bit architectures we need to check in state()
   // if state1 is aligned or not, and dynamically "swap" the field order if
   // needed.
   state1 uint64
   state2 uint32
}

我们可以得知:

  1. WaitGroup等待一组协程完成。  

  2. 主协程调用Add来设置要等待的goroutine的数量。

  3. 每个协程运行并在完成时调用Done。 

  4. 可以使用Wait来阻塞,直到所有协程完成

  5. WaitGroup在第一次使用后不能被复制。

那么此时我们已经清晰的看到了WaitGroup的使用方式

有Add类的方法,还有阻塞用的Wait方法

运行结束调用Done方法,我们来看看Done是什么

// 将 WaitGroup 计数器减一
// Done decrements the WaitGroup counter by one.
func (wg *WaitGroup) Done() {
   wg.Add(-1)
}

我们同步的精髓就是阻塞

我们来看一下阻塞Wait的源码

// Wait blocks until the WaitGroup counter is zero.
func (wg *WaitGroup) Wait() {
   statep, semap := wg.state()
   ……
   for {
      state := atomic.LoadUint64(statep)
      v := int32(state >> 32)
      w := uint32(state)
      if v == 0 {
         // Counter is 0, no need to wait.
         if race.Enabled {
            race.Enable()
            race.Acquire(unsafe.Pointer(wg))
         }
         return
      }
      // Increment waiters count.
      ……
   }

我们可以看出这个方法是用来阻塞代码直到计数器为0,当计数器为0时就不再阻塞了

原来是这样,看来Group是用一个计数器来操作的吖

根据上面的说法,我们来使用一下吧

import (
   "fmt"
   "sync"
)

// 声明一个等待组
var wp sync.WaitGroup


func main() {

   for i := 0; i < 10; i++ {
      go show(i)
      // 每执行一次就将计数器加一
      wp.Add(1)
   }

   
   wp.Wait()
   // 此时可以达到这样的效果:
   // 上面的所有协程都执行完,才到这一步
   fmt.Println("end...")
}

func show(i int) {
   // 我们看到Done的源码其实就是 Add(-1)
   // 此刻我们完成任务是就将计数器减一
   defer wp.Done()
   fmt.Println(i)
}

😎😎😎又是我,我是小小分割线

都用心看到这里了,那就求个赞吧😘

🥳🥳🥳如果小伙伴有其他的小知识,一定不要忘了在评论区讨论哟,多多讨论,生态才会越来越好