Channel的作用
Go中的channel的作用是:
- 在goroutine之间传递数据
- 让在发送或接收数据时被阻塞的goroutine变为runnable状态
Channel的结构
内部实现是hchan, 其内部维护了一个队列用来保存数据
type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
lock mutex
}
type waitq struct {
first *sudog
last *sudog
}
-
dataqsize: 数据队列大小, 即 make(chan T, N) 中的 N.
-
qcount: 队列中元素的个数
-
elemsize: 队列中单个元素的大小
-
buf: 一个循环队列,是实际存储数据的地方.(只用于buffered channel)
-
closed: 标识当前的channel是否为closed状态。通道创建后,该值为0,表示通道为打开状态。1表示通道关闭。
-
sendx 和 recvx : 循环队列当前的索引,分别表示发送数据和接收数据的位置
-
recvq 和 sendq: goroutine等待队列,
- recvq 用于存储因读取数据而阻塞的goroutine和
- sendq 用于存储因发送数据而阻塞的gorotuine。
-
lock: 锁定对channel的读写操作,因为发送和接收必须是互斥的
-
sudog 结构体
sudog 表示在等待队列中的goroutine。
type sudog struct { g *g next *sudog prev *sudog elem unsafe.Pointer // data element (may point to stack) isSelect bool ... c *hchan // channel }
recvq 和 sendq 的数据结构是一个链表,其结构如下图所示:
Recvq structure
c <- x 发送操作的执行逻辑:
- 往nil channel发送
if c == nil {
if !block {
return false
}
gopark(nil, nil, "chan send (nil chan)", traceEvGoStop, 2)
throw("unreachable")
}
如果往nil channel发送数据,当前goroutine会被阻塞。
- 往已关闭的channel发送
if c.closed != 0 {
unlock(&c.lock)
panic("send on closed channel")
}
如果往已关闭的channel发送数据,goroutine 会 panic
- 如果有goroutine在等待接收数据,那么数据会直接发送给等待的goroutine
if sg := c.recvq.dequeue(); sg != nil {
// Found a waiting receiver. We pass the value we want to send
// directly to the receiver, bypassing the channel buffer (if any).
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true
}
这就是 recvq 起作用的地方。recvq 中的goroutine是等待接收者,当前的channel上的写入操作会直接将值传给接收者。send函数的实现如下:
注意 396 行的 goready(gp, skip+1), 它会唤醒被阻塞的等待者。
- 如果缓存buf中还有剩余空间,那么就将数据放到缓冲buf中
chanbuf(c, i) 会访问响应的内存区域。
通过比较 qcount 和 dataqsize 来判断 hchan.buf 是否有剩余空间。入队操作就是将ep指针指向的数据拷贝到化环形缓冲区中以进行发送,并调整sendx和qcount.
5. hchan.buf 满了
acquireSudog 将当前的goroutine置为park状态,然后将其添加到channel的sendq 中
发送操作总结
-
锁定整个channel数据结构
-
判断是否写入。如果有recvq不为空,则直接写入到等待接收的goroutinte中;
-
如果recvq为空,判断缓冲区是否满了。如果没有满,则将数据从当前的goroutine拷贝到缓冲区中;
-
如果缓冲区满了,将数据保存到当前的goroutine中,并将当前的goroutine添加到 sendq 中并挂起。
这一点很有趣。
如果缓冲区满了,那么就将数据保存到当前正在执行的goroutine中。
这就是为什么无缓冲channel叫做“无缓冲”的原因,。因为对于无缓冲的 channel,如果没有没有接收者,然后你想要发送数据,那么该数据会被保存在
sudog的elem中。(也适用于带缓冲的channel)
记住go channels上的值传递都是通过值拷贝的方式
接收操作的执行逻辑 <- ch
接收操作的逻辑与发送操作很相似:
-
在nil channel上接收
不会报错,会阻塞
-
在 closed channel 接收
不会报错,不会阻塞
-
channel上的sendq被阻塞
从该 send goroutine 接收数据
-
chanbuf 非空
直接从chanbuf接收数据
-
chanbuf为空,也没有sender
阻塞
Select
Multiplexing on multiple channel.
select channel 例子
-
Operations are mutually exclusive, so need to acquire the locks on all involved channels in select case, which is done by sorting the cases by Hchan address to get the locking order, so that it does not lock mutexes of all involved channels at once.
-
操作是互斥的,所以需要获得所有涉及select case的通道的锁,这是通过对Hchan地址的排序来获得锁的顺序,这样就不会同时锁定所有涉及的通道。
sellock(scases, lockorder)scases数组中的每个scase都是一个结构,它包含了当前case中的操作种类和它所操作的通道。
kind 是当前case中的操作的种类, 可能是 CaseRecv, CaseSend 和 CaseDefault.
-
计算出轮询顺序,对所有参与的通道进行洗牌,以提供伪随机保证,并根据轮询顺序依次逐一遍历所有情况,看其中是否有准备好的通信。这种轮询顺序使选择操作不一定遵循程序中声明的顺序。
poll order
-
只要有一个通道的操作没有阻塞,选择语句就可以返回;如果所选的通道已经准备好了,甚至不需要接触所有的通道。
-
如果当前没有通道响应,也没有default语句,当前
g必须根据其情况挂在所有通道的相应的等待队列中。park goroutine in select case
sg.isSelect表示goroutine是否参与了select语句。 -
Select操作期间的接收、发送和关闭操作与通道上的接收、发送和关闭的一般操作类似。