Go 并发学习笔记

129 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第5天,点击查看活动详情

场景:协程A执行过程中需要创建子协程A1、A2、A3...An,协程A创建完子协程后就等待子协程退出。 针对这种场景,GO提供了三种解决方案:

  • Channel: 使用channel控制子协程
  • WaitGroup : 使用信号量机制控制子协程
  • Context: 使用上下文控制子协程

一个程序启动,就会有对应的进程被创建,同时进程也会移动一个线程,这个线程叫做主线程。如果主线程结束,那么整个程序就退出了。有了主线程,就可以在主线程程里启动很多其他线程,就有了多线程的并发

但是 Go 里面没有线程的概念,只有协程,相对于线程更加轻量,一个程序可以随意启动成千上万个协程,而协程是被 Go runtime 调度, Go 自己决定同时执行多少个协程,什么时候执行哪几个

Go 语言中提倡通过通信来共享内存,而不是通过共享内存来通信,其实就是体长通过 channel 发送接收消息的方式进行数据传递,而不是通过修改同一个变量。

通道

如果启动多个 goroutine, 它们之间的通信就需要 channel 来实现,通道主要用于共享内存

本质相当于一个 FIFO 队列

优点是实现比较简单,缺点是当需要大量创建协程的时候就需要相同数量的通道,对于子协程继续派生处理啊的协程不方便控制

创建方法

package main



import "fmt"



func main() {

    ch1 := make(chan int, 3)

    ch1 <- 2

    ch1 <- 1

    ch1 <- 3

    elem1 := <-ch1

    fmt.Printf("The first element received from channel ch1: %v\n",

    elem1)

}

其中 chan 表示通道,int 表示类型,而 3 表示通道的容量,也就是通道最多可以缓存多少个元素值

缓冲通道和非缓冲通道,其中缓冲通道的容量大于0,非缓冲通道的容量等于0,他们的区别在于有着不同的数据传递方式

发送语句使用箭头,接收语句也是使用箭头

发送和接收的特性

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

简单使用

func main() {



   ch:=make(chan string)



   go func() {



      fmt.Println("飞雪无情")



      ch <- "goroutine 完成"



   }()



   fmt.Println("我是 main goroutine")



   v:=<-ch



   fmt.Println("接收到的chan中的值为:",v)



}

结果是

我是 main goroutine



飞雪无情



接收到的chan中的值为: goroutine 完成

因为通过 make 创建的 chan 中没有值,而 main goroutine 又想从 chan 中获取值,获取不到就一直等待,等到另一个 goroutine 向 chan 发送值为止。所以程序不会在新的 goroutine 完成之前退出了

缓冲管道和无缓冲管道

无缓冲管道:容量为0,不存储任何数据,发送和接收操作是同时进行的,所以成为同步管道

缓冲管道:

cacheCh:=make(chan int,5)
  1. 内部有一个缓冲队列
  1. 发送操作是向队列的尾部插入元素,如果队列已满,则阻塞等待,直到另一个 goroutine 执行,接收操作释放队列的空间;
  2. 接收操作是从队列的头部获取元素并把它从队列中删除,如果队列为空,则阻塞等待,直到另一个 goroutine 执行,发送操作插入新的元素。

关闭管道

使用内置函数 close

如果一个 channel 被关闭了,就不能向里面发送数据了,如果发送的话,会引起 painc 异常。但是还可以接收 channel 里的数据,如果 channel 里没有数据的话,接收的数据是元素类型的零值

单向管道

只接收不发送

Goroutine

sync.WaitGroup

场景:某个 goroutine 需要等待其他几个 goroutine 全部完成

优点是 协程个数动态可调整

  1. 使用方法
  • wg.Add():main协程通过调用 wg.Add(delta int) 设置worker协程的个数,然后创建worker协程;
  • wg.Done():worker协程执行结束以后,都要调用 wg.Done(),表示做完任务,goroutine减1;
  • wg.Wait() :main协程调用 wg.Wait() 且被block,直到所有worker协程全部执行结束后返回。
  • 针对可能panic的goroutine,可以使用defer wg.Done()来结束goroutine。
  1. 例子
package main



import (

    "fmt"

    "time"

    "sync"

)



func main() {

    var wg sync.WaitGroup



    wg.Add(2) //设置计数器,数值即为goroutine的个数

    go func() {

        //Do some work

        time.Sleep(1*time.Second)



        fmt.Println("Goroutine 1 finished!")

        wg.Done() //goroutine执行结束后将计数器减1

    }()



    go func() {

        //Do some work

        time.Sleep(2*time.Second)



        fmt.Println("Goroutine 2 finished!")

        wg.Done() //goroutine执行结束后将计数器减1

    }()



    wg.Wait() //主goroutine阻塞等待计数器变为0

    fmt.Printf("All Goroutine finished!")

}

简单的说,上面程序中wg内部维护了一个计数器:

  • 启动goroutine前将计数器通过Add(2)将计数器设置为待启动的goroutine个数。
  • 启动goroutine后,使用Wait()方法阻塞自己,等待计数器变为0。
  • 每个goroutine执行结束通过Done()方法将计数器减1。
  • 计数器变为0后,阻塞的goroutine被唤醒。
  1. tips
  • Add()操作必须早于Wait(), 否则会panic;
  • Add()设置的值必须与实际等待的goroutine个数一致,否则会panic

Context

用于 goroutine 派生出子 goroutine,这时候用 waitGroup 就不太容易,因为子 goroutine 的个数不确定