Channel(管道)- 《Go 专家编程》笔记提要

管道(Channel)#

概念#

Go 在语言层提供的协程间通信的方式

初始化#

var ch chan int               // 声明管道
// 值为 nil

ch1 := make(chan int)         // 无缓冲管道
ch2 := make(chan int, 2)      // 带缓冲管道 

操作#

操作符#

操作符 “<-“ 表示数据流向,在函数间传递时可以用操作符限制管道的读写

ch <- 1                        // 向管道写入数据
<- ch                          // 从管道中读取数据

close(ch)                      // 关闭管道
// 尝试向关闭的管道写入数据会触发 panic
ch <- 2

// 但关闭的管道仍可读
// 第一个变量表示读出的数据,第二个变量(bool 类型)表示是否成功读取了数据,需要注意的是,第二个变量不用于知识管道的关闭状态
v, ok := <- ch

func ChanParamRW(ch chan int) {
  // 可读可写管道
}

func ChanParamR(ch <-chan int) {
  // 只读管道
}

func chanParamW(ch chan<- int) {
  // 只写管道
}

数据读写#

管道没有缓冲区,读写数据会阻塞,直到有协程向管道中写读数据。

有缓冲区但没有缓冲数据时读操作也会阻塞协程直到有数据写入才会唤醒该阻塞协程,向管道写入数据,缓冲区满了也会阻塞,直到有协程从缓冲区读取数据。

值为 nil 的管道无论读写都会阻塞,而且是永久阻塞。

使用 select 可以监控多个管道,当其中某一个管道可操作时就触发响应的 case 分支。事实上 select 语句的多个 case 语句的执行顺序是随机的。

其他操作#

内置函数 len()cap() 作用于管道,分别用于查询缓冲区中数据的个数以及缓冲区的大小。

管道实现了一种 FIFO(先入先出)的队列,数据总是按照写入的顺序流出管道。

协程读取管道时阻塞的条件有:

  • 管道无缓冲区
  • 管道的缓冲区中无数据
  • 管道的值为 nil

协程写入管道时阻塞的条件有:

  • 管道无缓冲区
  • 管道的缓冲区已满
  • 管道的值为 nil

实现原理#

数据结构#

src/runtime/chann.go: hchan

 32 type hchan struct {
 33         qcount   uint           // 当前队列中的剩余元素
 34         dataqsiz uint           // 环形队列长度,即可以存放的元素个数
 35         buf      unsafe.Pointer // 环形队列指针
 36         elemsize uint16                    // 每个元素的大小
 37         closed   uint32                    // 标识关闭状态
 38         elemtype *_type // 元素类型
 39         sendx    uint   // send index      队列下标,指示元素写入时存放到队列中的位置
 40         recvx    uint   // receive index 队列下标,指示下一个被读取的元素在队列中的位置
 41         recvq    waitq  // list of recv waiters    等待读消息的协程队列
 42         sendq    waitq  // list of send waiters 等待写消息的协程队列
 50         lock mutex            // 互斥锁, chan 不允许并发读写
 51 }

四个重点:

  • 环形队列
  • 等待队列(读写各一个)
  • 类型消息
  • 互斥锁

一般情况下 recvq 和 sendq 至少有一个为空。只有一个例外,那就是同一个协程使用 select 语句向管道一边写入数据一边读取数据,吃屎协程会分别位于两个等待队列中。

向管道写数据#

简单过程如下:

  • 如果缓冲区中有空余位置,则将数据写入缓冲区,结束发送过程。
  • 如果缓冲区中没有空余位置,则将当前协程加入 sendq 队列,并进入睡眠并等待被读协程唤醒

当接收队列 recvq 不为空时,说明缓冲区中没有数据但有协程在等待数据,此时会把数据直接传递给 recvq 队列的第一个协程,而不必再写入缓冲区。

从管道读数据#

简单过程如下:

  • 如果缓冲区中有数据,则从缓冲区中取出数据,结束读取过程。
  • 如果缓冲区没有数据,则将当前协程加入 recvq 队列,进入睡眠并等待被写协程唤醒。

类似的,如果等待发送队列 sendq 不为空,且没有缓冲区,那么直接从 sendq 队列的第一个协程中获取数据

关闭管道#

关闭管道会把 recvq 中的协程全部唤醒,这些协程或缺的数据都为 nil。 同时把 sendq 队列中的协程全部唤醒,但这些协程会触发 panic

除此之外,其他会触发 panic 的操作:

  • 关闭值为 nil 的管道
  • 关闭已经被关闭的管道
  • 向已经关闭的管道写入数据
本作品采用《CC 协议》,转载必须注明作者和本文链接