通道Channel

复习日志(1)

* 本页面主要介绍Go语言引用数据类型通道Channel的相关内容。

今天咱们总结的对象是:通道Channel!

一、通道是什么?

作为Go语言最有特色的数据类型,通道(channel)完全可以与协程(goroutine)并驾齐驱,共同代表 Go 语言独有的并发编程模式CSP(Communicating Sequential Processes)和编程哲学。

Don’t communicate by sharing memory; share memory by communicating.

不要通过共享内存来通信,而应该通过通信来共享内存。

通道类型完美诠释了这个编程理念的后半句:通过通信来共享内存!通过通道,我们可以在多个 goroutine 之间传递数据

当然,高并发是Go最具有自身特色的特性,所以我们将在第六章用一章的篇幅来详细总结高并发原理,今天这篇我们只看作为数据类型本身而言,通道的一些特性和需要注意的知识点。

通道(channel)是一种特殊的引用类型(零值为nil),它本身就是并发安全的(Go语言自带的唯一一个满足并发安全性的类型)。每个channel都有一个特殊的类型,也就是channel可发送数据的类型。

  //声明一个传递布尔型的通道
  //声明的通道后需要使用make函数初始化之后才能使用
  var ch1 chan bool 

  // 创建一个int类型的通道
  ch2 := make(chan int)

  // 创建一个string切片类型带有缓冲的通道,通道容量为5
  ch3 := make(chan []string, 5) 
二、通道的常见操作

作为goroutine之间的数据“沟通通道”,那可以想象到对于通道来说,一共有三种操作:发送(send)、接收(receive)和关闭(close)!另外对于有缓冲通道(下面讲到),还有两个常见操作获取元素数量(len)和通道容量(cap),平时使用场景非常少(因为在并发程序中该信息会随着接收操作而失效,但是它对某些故障诊断和性能优化会有帮助)。

  • 发送:发送语句将一个值从一个goroutine通过channel发送到另一个执行接收操作的goroutine
  • 接收:接收就是从一个通道中接收值,可以付给变量,也可以忽略
  • 关闭:使用内置的close函数可以关闭一个channel。对基于已关闭的channel的任何发送和再关闭操作都将导致panic异常,但是依旧可以接收,会接收到零值。另外,和关闭文件不同,通道关闭是可选的,因为通道是可以被垃圾回收机制回收的
  • 获取通道内元素数量:使用内置的len函数
  • 获取通道容量:使用内置的cap函数
  //发送
  ch <- x 

  // 接收
  x = <-ch

  // 接收,但忽略接收到的值
  <-ch 

  // 关闭
  close(ch) 

  //判断通道关闭的方法
  //通道关闭后再取值ok=false
  //但是这个值会有延迟,比如通道还有数据但已经关闭时,ok还是true
  i, ok := <-ch1 
  if !ok {
      ……
  }

  //通道关闭后会退出for range循环
  for i := range ch2 { 
      fmt.Println(i)
  }

一个通道相当于一个先进先出(FIFO)的队列。可以看到上面的接送操作符<-的方向完美的展示出了“消息的去向”

对于通道的基本操作,有以下基本特性:

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

需要强调的是,进入通道的并不是在接收操作符右边的那个元素值,而是它的副本,所以就是两步操作,复制元素副本,把副本放到通道里。而从通道出来时,实际上也有两步操作:第一步是生成正在通道中的这个元素值的副本,并准备给到接收方,第二步是删除在通道中的这个元素值。上面特性的第二条也就是在强调这“发送”和“接收”里面的“子操作”都是一气呵成不可打断的。

三、通道分类
  • 无缓冲通道:又称为阻塞的通道,或同步通道,因为使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。就像在没有快递柜之前,快递员必须打电话把快递包裹亲手交给你一样,是必须同步进行的。
  • 使用无缓冲通道在goroutine之间同步数据
    使用无缓冲通道在goroutine之间同步数据
  • 有缓冲的通道:和无缓冲通道不同的是,它有一个可以缓冲指定大小(称为容量)的空间“暂时存放”要传递的值。这时候就好比小区里安装了一个快递柜,能存放100个包裹,假如还有空位,快递小哥就会电话告诉你把包裹放在几号柜子里,你有时间来取就可以了。
  • 使用有缓冲通道在goroutine之间同步数据
    使用有缓冲通道在goroutine之间同步数据
  • 单向通道:被限制了只能发送或接收的通道。主要是用来约束其他代码的行为。在函数传参及任何赋值操作中将双向通道转换为单向通道是可以的,但反过来是不可以的。
  •   func squarer(out chan<- int, in <-chan int) {
          for i := range in {
              out <- i * i
          }
          close(out)
      }

上面代码中chan<- int是一个只能发送的通道,可以发送但是不能接收;<-chan int是一个只能接收的通道,可以接收但是不能发送。

这里有一个优良实践,有些消息事件并不携带额外的信息,它仅仅是用作两个goroutine之间的同步,这时候我们可以用struct{}空结构体作为channels元素的类型,虽然也可以使用bool或int类型实现同样的功能,done <- 1语句也比done <- struct{}{}更短,但是空结构体带来的性能开销更小

四、通道引起阻塞

了解了通道操作的基本特性之后我们可以发现,基于这些特性,在对通道进行操作的时候就可能会产生代码阻塞。那都在哪些场景下可能会产生长时间的代码阻塞呢?

正常使用通道过程中可能产生的阻塞:
  • 对于有缓冲通道:
    • 通道已满:对它的所有发送操作都会被阻塞,直到通道中有元素值被接收走
    • 通道已空:对它的所有接收操作都会被阻塞,直到通道中有新的元素值出现
  • 对于无缓冲通道:无论是发送操作还是接收操作,一开始执行就会被阻塞,直到配对的操作也开始执行,才会继续传递。因为它用的是“同步”方式传递数据(想想上面接收快递的场景)。并且,数据是直接从发送方复制到接收方的,中间并不会用非缓冲通道做中转
  •   func main() {
        pipline := make(chan string)
        pipline <- "hello world"
        fmt.Println(<-pipline)
      }
    
      //fatal error: all goroutines are asleep - deadlock! 
    
      //上面的代码就是无缓冲通道,在接受者未准备好的情况下就发送会造成死锁
      //解决方法可以是先让接收者执行
      //当然也可以换成有缓冲通道
      func main() {
        pipline := make(chan string)
        fmt.Println(<-pipline)
        pipline <- "hello world"
      } 
错误使用通道过程中可能产生的阻塞:
  • 对于值为nil的通道:对它的发送操作和接收操作都会永久地处于阻塞状态。它们所属的 goroutine 中的任何代码,都不再会被执行。所以一定要记得用make初始化通道之后在使用!
五、通道引起panic

对通道的操作何时会引起panic,我们特别做一下总结:

  • 通道已关闭:对它的发送和关闭操作,都会引起panic

对于通道类型的大致内容就总结到这里,你都Get了吗?


* 本页内容参考以下数据源:

  • 《Go程序设计语言》
  • 《Go语言核心36讲》 10 通道的基本操作
  • https://www.cnblogs.com/wongbingming/p/13035179.html

凯冰科技 · 代码改变世界,技术改变生活
Next Page→