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