channelchannel
channel
channel

来看看咱们平常传递消息的需求:golang

channelchannelchannel
channel
channel

无缓冲

先把示例代码贴出来。就是两个读的 goroutine 被阻塞在一个无缓冲的 channel 上。缓存

func main() {
 ch := make(chan int) // 无缓冲   go goRoutineA(ch)  go goRoutineB(ch)  ch <- 1   time.Sleep(time.Second * 1) }  func goRoutineA(ch chan int) {  v := <-ch  fmt.Printf("A received data: %d\n", v) }  func goRoutineB(ch chan int) {  v := <-ch  fmt.Printf("B received data: %d\n", v) } 复制代码
ch <- 1channel
无缓冲 channel
无缓冲 channel
buf
交换数据
交换数据

上图描述的是数据交换过程,再看一下读 goroutine 被阻塞的结构示意图。被阻塞的 goroutine 会挂载到对应的队列上,该队列是一个双端队列。异步

sendq
结构示例
结构示例

有缓冲

咱们将上面的代码改为有缓冲的通道,而后再来看看有缓冲的状况。

func main() {
 ch := make(chan int, 3) // 有缓冲   // 都不会阻塞  ch <- 1  ch <- 2  ch <- 3  // 会阻塞,被挂起到 sendq 中  go func() {  ch <- 4  }()   // 只是为了debug  var a int  fmt.Println(a)   go goRoutineA(ch)  go goRoutineA(ch)  go goRoutineB(ch)  go goRoutineB(ch) // 猜猜这里会被挂起吗?   time.Sleep(time.Second * 2) }  func goRoutineA(ch chan int) {  v := <-ch  fmt.Printf("A received data: %d\n", v) }  func goRoutineB(ch chan int) {  v := <-ch  fmt.Printf("B received data: %d\n", v) } 复制代码
go goRoutineA(ch)hchan
有缓冲 channel
有缓冲 channel
sendq

其实有缓冲的 channel,就是把同步的通讯变为了异步的通讯。写的 channel 不须要关注读 channel,只要有空间它就写;而读也同样,只要有数据就正常读就能够,若是没有就挂起到队列中,等待被唤醒。下图形象的展现了有缓冲 channel 是如何交换数据的。

交换数据
交换数据

咱们再来用图的形式看一下此时结构体的样子,这里图有些偷懒,只是在上面图的基础上增长了循环队列部分的描述,实际到该例子中,读 goroutine时不会被阻塞的,看的时候须要注意这一点。

结构示例
结构示例

循环队列

今天最重要的是理解 channel 中两个关键的数据结构。为了下一讲阅读源码作准备,我把 channel 中的循环队列部分的代码抽象出来了。

// 队列满了
var ErrQFull = errors.New("circular is full")  // 没有值 var ErrQEmpty = errors.New("circular is empty")  // 定义循环队列 // 如何肯定队空,仍是队满?q.sendx = (q.sendx+1) % q.dataqsiz type queue struct {  buf []int // 队列元素存储  dataqsiz uint // circular 队列长度  qcount uint // 有多少元素在buf中 qcount = len(buf)  sendx uint // 能够理解为队尾指针,向队列写入数据  recvx uint // 能够理解为队头指针,从队列读取数据 }  func makeQ(size int) *queue {  q := &queue{  dataqsiz: uint(size),  buf: nil,  }   q.buf = make([]int, q.dataqsiz)   return q }  // 向buf中写入数据 // 请看 chansend 函数 func (c *queue) insert(ele int) error {  // 检查队列是否有空间  if c.dataqsiz > 0 && c.qcount == c.dataqsiz {  return ErrQFull  }   // 存入数据  c.buf[c.sendx] = ele  c.sendx++ // 尾指针后移  if c.sendx == c.dataqsiz { // 若是相等,说明队列写满了,sendx放到开始位置  c.sendx = 0  }  c.qcount++   return nil }  // 从buf中读取数据 func (c *queue) read() (int, error) {  // 队列中没有数据了  if c.dataqsiz > 0 && c.qcount == 0 {  return 0, ErrQEmpty  }   ret := c.buf[c.recvx] // 取出元素  c.buf[c.recvx] = 0  c.recvx++  if c.recvx == c.dataqsiz { // 若是相等,说明写到末尾了,recvx放到开始位置  c.recvx = 0  }  c.qcount--   return ret, nil } 复制代码
channel(tail+1)%n=head

总结一下今天的主要信息。

  1. channel 中用到了两个数据结构: 循环队列双端链表
  2. 循环队列 只有在有缓冲 channel 中才会使用,它主要是作为消息的缓冲、保证消息的有序性;
  3. 双端链表 是用来挂起阻塞的读、写 goroutine 的,在被唤醒时会按照入队顺序公平的进行通知;
  4. 无缓冲的 channel 不会用到 循环队列 相关的结构,它必须读写 goroutine 都准备好后才能进行消息交换;
  5. 作为缓冲消息的 循环队列 经过一个当前元素个数字段的标记,避免了浪费一个数据空间。
channel

参考资料