在Go语言中,channel通道用于多个goroutine间通信。
有关goroutine和channel的基本概念和使用在本人的《Go入门系列 go并发编程之Goroutine与channel》的系列文章中有介绍,下面是传送门。
http://zbpblog.com/blog-231.html
http://zbpblog.com/blog-232.html
http://zbpblog.com/blog-233.html
在本篇文章中,本人就不再对goroutine 和 channel的基本使用做重复的介绍和讲解,本篇文章的重点是介绍channel的实现原理和一些补充性的channel用法。下面内容转载自
1.chan的数据结构
src/runtime/chan.go:hchan定义了channel的数据结构:
type hchan struct {
qcount uint // 当前队列中剩余元素个数
dataqsiz uint // 环形队列长度,即可以存放的元素个数
buf unsafe.Pointer // 环形队列指针
elemsize uint16 // 每个元素的大小
closed uint32 // 标识关闭状态
elemtype *_type // 元素类型
sendx uint // 队列下标,指示元素写入时存放到队列中的位置
recvx uint // 队列下标,指示元素从队列的该位置读出
recvq waitq // 等待读消息的goroutine队列
sendq waitq // 等待写消息的goroutine队列
lock mutex // 互斥锁,chan不允许并发读写
}
从这个结构体可以看出,一个channel通道由一个环形队列,goroutine队列,互斥锁以及各种状态标识和类型信息组成。
环形队列
chan内部使用一个环形队列作为其缓冲区,队列长度是make这个chan的时候指定的。
其中
buf成员指向这个环形队列的内存地址
dataqsiz成员表示这个队列的容量
qcount成员表示这个队列剩余的空闲位置个数
sendx和recvx分别是这个环形队列的写指针和读指针(写入的位置和读取的位置)
比较有意思的是sendx和recvx,当goroutine往一个chan发送数据时,sendx写指针就会移动到下一个元素,读指针不变;而从一个chan接收数据时,读指针也会移到下一个元素的位置,写指针不变。例如一个缓冲区长度为4,类型为int的channel,G1负责发送,G2负责接收:
一开始sendx和recvx都指向第一个元素
我们看看下面的几种状况:
A. G1一口气发送5个数字(1,2,3,4,5)但G2不接收的情况下,发送前4个数字的时候都一切顺利,但是发送第5个数字的时候,由于缓冲区是个环形队列,因此此时写指针重新回到了第一个元素与recvx重合,在发送5的时候,由于队列已满就会发生阻塞。
然后此时G2开始接收2次,recvx会往后移动2个位置,sendx发现缓冲区中又有空余位置后写入5并往下移动1个位置。第一个位置的元素值1会被5覆盖:
B. G1先发送1,2,3, 然后G2接收4次,如下图
G2读取过的元素在缓冲区中是不会马上被清除的,但是会被循环往复的写操作覆盖,就像上面的例子中5把1给覆盖掉(环形队列的特性)
等待的goroutine队列
如果一个goroutine(简称G)在发送的时候被chan(简称C)阻塞,那么这个G就会将状态置为Gwaiting并从当前内核线程(简称M)切换出来,放入到C的等待写消息的goroutine队列sendq。
同理如果一个G从chan接收数据被阻塞的话,这个G会被放到这个C的等待读消息的goroutine队列recvq。
当这些G被唤醒后会重新回到调度器或上下文环境P的可运行G队列。
sendq和recvq的数据结构是双向链表。
一般情况下recvq和sendq至少有一个为空。只有一个例外,那就是同一个goroutine使用select语句向channel一边写数据,一边读数据。
chan中的锁
我们知道,chan用于为多个goroutine并发通信,因此往chan读写元素就会涉及到并发读写的数据安全问题。而chan中的成员lock就是为了保证chan读写时的并发安全而设的。每一次的读和写操作都会先对lock上锁,使得一个goroutine对这个chan发送或接收数据的同一时刻其他goroutine不能对该chan发送和接收。
向chan读或写数据的简单流程
关闭chan
关闭channel时会把recvq中的G全部唤醒,并且如果关闭的时候channel中已经没有数据可接收的话,这些G接收到的数据全为nil。
关闭channel也会把sendq中的G全部唤醒,但这些G会panic。