并发编程之Channel | 青训营笔记

113 阅读7分钟

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

Channel

Go语言采用的并发模型是 CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信

如果说 goroutine 是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制。

Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

channel类型

channel是 Go 语言中一种特有的类型。声明通道类型变量的格式如下:

 var 变量名称 chan 元素类型

其中:

  • chan:是关键字
  • 元素类型:是指通道中传递元素的类型

举几个例子:

 var ch1 chan int   // 声明一个传递整型的通道
 var ch2 chan bool  // 声明一个传递布尔型的通道
 var ch3 chan []int // 声明一个传递int切片的通道

未初始化的通道类型变量其默认零值是nil

 var ch chan int
 fmt.Println(ch) // <nil>

初始化channel

声明的通道类型变量需要使用内置的make函数初始化之后才能使用。具体格式如下:

 make(chan 元素类型, [缓冲大小])

其中:

  • channel的缓冲大小是可选的。

举几个例子:

 ch4 := make(chan int)
 ch5 := make(chan bool, 1)  // 声明一个缓冲区大小为1的通道

channel操作

通道共有发送(send)、接收(receive)和关闭(close)三种操作。而发送和接收操作都使用<-符号。

现在我们先使用以下语句定义一个通道:

 ch := make(chan int)

将一个值发送到通道中。

 ch <- 10 // 把10发送到ch中

从一个通道中接收值。

 x := <- ch // 从ch中接收值并赋值给变量x
 <-ch       // 从ch中接收值,忽略结果

我们通过调用内置的close函数来关闭通道。

 close(ch)

注意:一个通道值是可以被垃圾回收掉的。通道通常由发送方执行关闭操作,并且只有在接收方明确等待通道关闭的信号时才需要执行关闭操作。它和关闭文件不一样,通常在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。

关闭后的通道有以下特点:

  1. 对一个关闭的通道再发送值就会导致 panic。
  2. 对一个关闭的通道进行接收会一直获取值直到通道为空。
  3. 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
  4. 关闭一个已经关闭的通道会导致 panic。

无缓冲的通道

img

无缓冲的通道又称为阻塞的通道。示例:

 func main() {
     ch := make(chan int)
     ch <- 10
     fmt.Println("发送成功")
 }

上面这段代码能够通过编译,但是执行的时候会出现以下错误:

 fatal error: all goroutines are asleep - deadlock!
 ​
 goroutine 1 [chan send]:
 main.main()
         .../main.go:8 +0x54

deadlock表示我们程序中的 goroutine 都被挂起导致程序死锁了。为什么会出现deadlock错误呢?

因为我们使用ch := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有接收方能够接收值的时候才能发送成功,否则会一直处于等待发送的阶段。同理,如果对一个无缓冲通道执行接收操作时,没有任何向通道中发送值的操作那么也会导致接收操作阻塞。简单来说就是无缓冲的通道必须有至少一个接收方才能发送成功。

上面的代码会阻塞在ch <- 10这一行代码形成死锁,那如何解决这个问题呢?

其中一种可行的方法是创建一个 goroutine 去接收值,例如:

 func recv(c chan int) {
     ret := <-c
     fmt.Println("接收成功", ret)
 }
 ​
 func main() {
     ch := make(chan int)
     go recv(ch) // 创建一个 goroutine 从通道接收值
     ch <- 10
     fmt.Println("发送成功")
 }

首先无缓冲通道ch上的发送操作会阻塞,直到另一个 goroutine 在该通道上执行接收操作,这时数字10才能发送成功,两个 goroutine 将继续执行。相反,如果接收操作先执行,接收方所在的 goroutine 将阻塞,直到 main goroutine 中向该通道发送数字10。

使用无缓冲通道进行通信将导致发送和接收的 goroutine 同步化。因此,无缓冲通道也被称为同步通道

有缓冲的通道

还有另外一种解决上面死锁问题的方法,那就是使用有缓冲区的通道。

img

我们可以在使用 make 函数初始化通道时,可以为其指定通道的容量,例如:

 func main() {
     ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
     ch <- 10
     fmt.Println("发送成功")
 }

只要通道的容量大于零,那么该通道就属于有缓冲的通道,通道的容量表示通道中最大能存放的元素数量。当通道内已有元素数达到最大容量后,再向通道执行发送操作就会阻塞,除非有从通道执行接收操作。就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。

我们可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量,虽然我们很少会这么做。

从通道中循环取值

当通过通道发送有限的数据时,我们可以通过close函数关闭通道来告知从该通道接收值的goroutine停止等待。当通道被关闭时,往该通道发送值会引发panic,从该通道里接收的值一直都是类型零值。那如何判断一个通道是否被关闭了呢?

下面示例给出两种方法:

 package main
 ​
 import "fmt"
 ​
 // 方式1:多返回值模式
 func f1(ch chan int) {
     for {
         v, ok := <-ch
         if !ok {
             fmt.Println("通道已关闭")
             break
         }
         fmt.Printf("v: %#v ok: %#v\n", v, ok)
     }
 }
 ​
 // 方式2:for range接收(常用)
 func f2(ch chan int) {
     for v := range ch {
         fmt.Println(v)
     }
 }
 ​
 func main() {
     ch := make(chan int, 2)
     ch <- 1
     ch <- 2
     close(ch)
     f1(ch)
     f2(ch)
 }

输出:

 v: 1 ok: true
 v: 2 ok: true
 通道已关闭

注意:目前Go语言中并没有提供一个不对通道进行读取操作就能判断通道是否被关闭的方法。不能简单的通过len(ch)操作来判断通道是否被关闭。

单向通道

在某些场景下我们可能会将通道作为参数在多个任务函数间进行传递,通常我们会选择在不同的任务函数中对通道的使用进行限制,比如限制通道在某个函数中只能执行发送或只能执行接收操作。

Go语言中提供了单向通道来处理这种需要限制通道只能进行某种操作的情况:

 <- chan int     // 只接收通道,只能接收不能发送
 chan <- int     // 只发送通道,只能发送不能接收

其中,箭头<-和关键字chan的相对位置表明了当前通道允许的操作,这种限制将在编译阶段进行检测。另外对一个只接收通道执行close也是不允许的,因为默认通道的关闭操作应该由发送方来完成。

示例:

 package main
 ​
 import "fmt"
 ​
 // 生成0-100的数字发送到ch1中
 func counter(ch chan<- int) {
     for i := 0; i < 100; i++ {
         ch <- i
     }
     close(ch)
 }
 ​
 // 从ch1中取出数据计算它的平方,把结果发送到ch2中
 func squarer(ch1 <-chan int, ch2 chan<- int) {
     for i := range ch1 {
         ch2 <- i * i
     }
     close(ch2)
 }
 ​
 // 打印通道中的值
 func printer(ch <-chan int) {
     for i := range ch {
         fmt.Println(i)
     }
 }
 ​
 func main() {
     ch1 := make(chan int, 100)
     ch2 := make(chan int, 100)
     go counter(ch1)
     go squarer(ch1, ch2)
     printer(ch2)
 }

运行测试,成功输出0-100平方后的数值。

总结

image-20220507194919278

注意: 对已经关闭的通道再执行 close 也会引发 panic。