https://golang.design/under-the-hood/zh-cn/part1basic/ch03lang/chan/
Channel 底层结构
实现 Channel 的结构并不神秘,本质上就是一个 mutex
锁加上一个环状缓存、 一个发送方队列和一个接收方队列:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// src/runtime/chan.go
type hchan struct {
qcount uint // 队列中的所有数据数
dataqsiz uint // 环形队列的大小
buf unsafe.Pointer // 指向大小为 dataqsiz 的数组
elemsize uint16 // 元素大小
closed uint32 // 是否关闭
elemtype *_type // 元素类型
sendx uint // 发送索引
recvx uint // 接收索引
recvq waitq // recv 等待列表,即( <-ch )
sendq waitq // send 等待列表,即( ch<- )
lock mutex
}
type waitq struct { // 等待队列 sudog 双向队列
first *sudog
last *sudog
}
|
图1:Channel 的结构
其中 recvq
和 sendq
分别是 sudog
的一个链式队列, 其元素是一个包含当前包含队 Goroutine 及其要在 Channel 中发送的数据的一个封装, 如图 1 所示。
更多关于 sudog 的细节,请参考 6.8 同步原语。
Channel 的创建
Channel 的创建语句由编译器完成如下翻译工作:
1
|
make(chan type, n) => makechan(type, n)
|
将一个 make
语句转换为 makechan
调用。 而具体的 makechan
实现的本质是根据需要创建的元素大小, 对 mallocgc
进行封装, 因此,Channel 总是在堆上进行分配,它们会被垃圾回收器进行回收, 这也是为什么 Channel 不一定总是需要调用 close(ch)
进行显式地关闭。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
// src/runtime/chan.go
// 将 hchan 的大小对齐
const hchanSize = unsafe.Sizeof(hchan{}) + uintptr(-int(unsafe.Sizeof(hchan{}))&7)
func makechan(t *chantype, size int) *hchan {
elem := t.elem
...
// 检查确认 channel 的容量不会溢出
mem, overflow := math.MulUintptr(elem.size, uintptr(size))
if overflow || mem > maxAlloc-hchanSize || size < 0 {
panic("makechan: size out of range")
}
var c *hchan
switch {
case mem == 0:
// 队列或元素大小为零
c = (*hchan)(mallocgc(hchanSize, nil, true))
...
case elem.ptrdata == 0:
// 元素不包含指针
// 在一个调用中分配 hchan 和 buf
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
// 元素包含指针
c = new(hchan)
c.buf = mallocgc(mem, elem, true)
}
c.elemsize = uint16(elem.size)
c.elemtype = elem
c.dataqsiz = uint(size)
...
return c
}
|
Channel 并不严格支持 int64
大小的缓冲,当 make(chan type, n)
中 n 为 int64
类型时, 运行时的实现仅仅只是将其强转为 int
,提供了对 int
转型是否成功的检查:
1
2
3
4
5
6
7
8
9
|
// src/runtime/chan.go
func makechan64(t *chantype, size int64) *hchan {
if int64(int(size)) != size {
panic("makechan: size out of range")
}
return makechan(t, int(size))
}
|
所以创建一个 Channel 最重要的操作就是创建 hchan
以及分配所需的 buf
大小的内存空间。
向 Channel 发送数据
发送数据完成的是如下的翻译过程:
1
|
ch <- v => chansend1(ch, v)
|
而本质上它会去调用更为通用的 chansend
:
1
2
3
4
|
//go:nosplit
func chansend1(c *hchan, elem unsafe.Pointer) {
chansend(c, elem, true)
}
|
下面我们来关注 chansend
的具体实现的第一个部分:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
func chansend(c *hchan, ep unsafe.Pointer, block bool) bool {
// 当向 nil channel 发送数据时,会调用 gopark
// 而 gopark 会将当前的 Goroutine 休眠,从而发生死锁崩溃
if c == nil {
if !block {
return false
}
gopark(nil, nil, waitReasonChanSendNilChan)
throw("unreachable")
}
...
}
|
在这个部分中,我们可以看到,如果一个 Channel 为零值(比如没有初始化),这时候的发送操作会暂止当前的 Goroutine(gopark
)。 而 gopark 会将当前的 Goroutine 休眠,从而发生死锁崩溃。
现在我们来看一切已经准备就绪,开始对 Channel 加锁:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
func chansend(c *hchan, ep unsafe.Pointer, block bool) bool {
...
lock(&c.lock)
// 持有锁之前我们已经检查了锁的状态,
// 但这个状态可能在持有锁之前、该检查之后发生变化,
// 因此还需要再检查一次 channel 的状态
if c.closed != 0 { // 不允许向已经 close 的 channel 发送数据
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
// 1. channel 上有阻塞的接收方,直接发送
if sg := c.recvq.dequeue(); sg != nil {
send(c, sg, ep, func() { unlock(&c.lock) })
return true
}
// 2. 判断 channel 中缓存是否有剩余空间
if c.qcount < c.dataqsiz {
// 有剩余空间,存入 c.buf
qp := chanbuf(c, c.sendx)
...
typedmemmove(c.elemtype, qp, ep) // 将要发送的数据拷贝到 buf 中
c.sendx++
if c.sendx == c.dataqsiz { // 如果 sendx 索引越界则设为 0
c.sendx = 0
}
c.qcount++ // 完成存入,记录增加的数据,解锁
unlock(&c.lock)
return true
}
if !block {
unlock(&c.lock)
return false
}
...
}
|
到目前位置,代码中考虑了当 Channel 上有接收方等待,可以直接将数据发送走,并返回(情况 1);或没有接收方 但缓存中还有剩余空间来存放没有读取的数据(情况 2)。对于直接发送数据的情况,由 send
调用完成:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func()) {
...
if sg.elem != nil {
sendDirect(c.elemtype, sg, ep)
sg.elem = nil
}
gp := sg.g
unlockf() // unlock(&c.lock)
gp.param = unsafe.Pointer(sg)
...
// 复始一个 Goroutine,放入调度队列等待被后续调度
goready(gp) // 将 gp 作为下一个立即被执行的 Goroutine
}
func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) {
dst := sg.elem
... // 为了确保发送的数据能够被立刻观察到,需要写屏障支持,执行写屏障,保证代码正确性
memmove(dst, src, t.size) // 直接写入接收方的执行栈!
}
|
send
操作其实是隐含了有接收方阻塞在 Channel 上,换句话说有接收方已经被暂止, 当我们发送完数据后,应该让该接收方就绪(让调度器继续开始调度接收方)。
这个 send
操作其实是一种优化。原因在于,已经处于等待状态的 Goroutine 是没有被执行的, 因此用户态代码不会与当前所发生数据发生任何竞争。我们也更没有必要冗余的将数据写入到缓存, 再让接收方从缓存中进行读取。因此我们可以看到, sendDirect
的调用, 本质上是将数据直接写入接收方的执行栈。
最后我们来看第三种情况,如果既找不到接收方,buf
也已经存满, 这时我们就应该阻塞当前的 Goroutine 了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
func chansend(c *hchan, ep unsafe.Pointer, block bool) bool {
...
// 3. 阻塞在 channel 上,等待接收方接收数据
gp := getg()
mysg := acquireSudog()
...
c.sendq.enqueue(mysg)
gopark(chanparkcommit, unsafe.Pointer(&c.lock)) // 将当前的 g 从调度队列移出
// 因为调度器在停止当前 g 的时候会记录运行现场,当恢复阻塞的发送操作时候,会从此处继续开始执行
...
gp.waiting = nil
gp.activeStackChans = false
if gp.param == nil {
if c.closed == 0 { // 正常唤醒状态,Goroutine 应该包含需要传递的参数,但如果没有唤醒时的参数,且 channel 没有被关闭,则为虚假唤醒
throw("chansend: spurious wakeup")
}
panic(plainError("send on closed channel"))
}
gp.param = nil
...
mysg.c = nil // 取消与之前阻塞的 channel 的关联
releaseSudog(mysg) // 从 sudog 中移除
return true
}
func chanparkcommit(gp *g, chanLock unsafe.Pointer) bool {
// 具有未解锁的指向 gp 栈的 sudog。栈的复制必须锁住那些 sudog 的 channel
gp.activeStackChans = true
unlock((*mutex)(chanLock))
return true
}
|
简单总结一下,发送过程包含三个步骤:
- 持有锁
- 入队,拷贝要发送的数据
- 释放锁
其中第二个步骤包含三个子步骤:
- 找到是否有正在阻塞的接收方,是则直接发送
- 找到是否有空余的缓存,是则存入
- 阻塞直到被唤醒
从 Channel 接收数据
接收数据主要是完成以下翻译工作:
1
2
|
v <- ch => chanrecv1(ch, v)
v, ok <- ch => ok := chanrecv2(ch, v)
|
他们的本质都是调用 chanrecv
:
1
2
3
4
5
6
7
8
9
|
//go:nosplit
func chanrecv1(c *hchan, elem unsafe.Pointer) {
chanrecv(c, elem, true)
}
//go:nosplit
func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
_, received = chanrecv(c, elem, true)
return
}
|
chanrecv 的具体实现如下,由于我们已经仔细分析过发送过程了, 我们不再详细分拆下面代码的步骤,其处理方式基本一致:
- 上锁
- 从缓存中出队,拷贝要接收的数据
- 解锁
其中第二个步骤包含三个子步骤:
- 如果 Channel 已被关闭,且 Channel 没有数据,立刻返回
- 如果存在正在阻塞的发送方,说明缓存已满,从缓存队头取一个数据,再复始一个阻塞的发送方
- 否则,检查缓存,如果缓存中仍有数据,则从缓存中读取,读取过程会将队列中的数据拷贝一份到接收方的执行栈中
- 没有能接受的数据,阻塞当前的接收方 Goroutine
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
|
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
...
// nil channel,同 send,会导致两个 Goroutine 的死锁
if c == nil {
if !block {
return
}
gopark(nil, nil, waitReasonChanReceiveNilChan)
throw("unreachable")
}
// 快速路径: 在不需要锁的情况下检查失败的非阻塞操作
//
// 注意到 channel 不能由已关闭转换为未关闭,则
// 失败的条件是:1. 无 buf 时发送队列为空 2. 有 buf 时,buf 为空
// 此处的 c.closed 必须在条件判断之后进行验证,
// 因为指令重排后,如果先判断 c.closed,得出 channel 未关闭,无法判断失败条件中
// channel 是已关闭还是未关闭(从而需要 atomic 操作)
if !block && (c.dataqsiz == 0 && c.sendq.first == nil ||
c.dataqsiz > 0 && atomic.Loaduint(&c.qcount) == 0) &&
atomic.Load(&c.closed) == 0 {
return
}
...
lock(&c.lock)
// 1. channel 已经 close,且 channel 中没有数据,则直接返回
if c.closed != 0 && c.qcount == 0 {
...
unlock(&c.lock)
if ep != nil {
typedmemclr(c.elemtype, ep)
}
return true, false
}
// 2. channel 上有阻塞的发送方,直接接收
if sg := c.sendq.dequeue(); sg != nil {
recv(c, sg, ep, func() { unlock(&c.lock) })
return true, true
}
// 3. channel 的 buf 不空
if c.qcount > 0 {
// 直接从队列中接收
qp := chanbuf(c, c.recvx)
...
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
typedmemclr(c.elemtype, qp)
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.qcount--
unlock(&c.lock)
return true, true
}
if !block {
unlock(&c.lock)
return false, false
}
// 4. 没有数据可以接收,阻塞当前 Goroutine
gp := getg()
mysg := acquireSudog()
...
c.recvq.enqueue(mysg)
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive)
...
// 被唤醒
gp.waiting = nil
...
closed := gp.param == nil
gp.param = nil
mysg.c = nil
releaseSudog(mysg)
return true, !closed
}
|
接收数据同样包含直接往接收方的执行栈中拷贝要发送的数据,但这种情况当且仅当缓存大小为0时(即无缓冲 Channel)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
if c.dataqsiz == 0 {
...
if ep != nil {
// 直接从对方的栈进行拷贝
recvDirect(c.elemtype, sg, ep)
}
} else {
// 从缓存队列拷贝
qp := chanbuf(c, c.recvx)
...
// 从队列拷贝数据到接收方
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
// 从发送方拷贝数据到队列
typedmemmove(c.elemtype, qp, sg.elem)
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
}
sg.elem = nil
gp := sg.g
unlockf()
gp.param = unsafe.Pointer(sg)
...
goready(gp, skip+1)
}
|
到目前为止我们终于明白了为什么无缓冲 Channel 而言 v <- ch
happens before ch <- v
了, 因为无缓冲 Channel 的接收方会先从发送方栈拷贝数据后,发送方才会被放回调度队列中,等待重新调度。
Channel 的关闭
关闭 Channel 主要是完成以下翻译工作:
1
|
close(ch) => closechan(ch)
|
具体的实现中,首先对 Channel 上锁,而后依次将阻塞在 Channel 的 g 添加到一个 gList 中,当所有的 g 均从 Channel 上移除时,可释放锁,并唤醒 gList 中的所有接收方和发送方:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
func closechan(c *hchan) {
if c == nil { // close 一个空的 channel 会 panic
panic(plainError("close of nil channel"))
}
lock(&c.lock)
if c.closed != 0 { // close 一个已经关闭的的 channel 会 panic
unlock(&c.lock)
panic(plainError("close of closed channel"))
}
...
c.closed = 1
var glist gList
// 释放所有的接收方
for {
sg := c.recvq.dequeue()
if sg == nil { // 队列已空
break
}
if sg.elem != nil {
typedmemclr(c.elemtype, sg.elem) // 清零
sg.elem = nil
}
...
gp := sg.g
gp.param = nil
...
glist.push(gp)
}
// 释放所有的发送方
for {
sg := c.sendq.dequeue()
if sg == nil { // 队列已空
break
}
sg.elem = nil
...
gp := sg.g
gp.param = nil
...
glist.push(gp)
}
// 释放 channel 的锁
unlock(&c.lock)
// 就绪所有的 G
for !glist.empty() {
gp := glist.pop()
gp.schedlink = 0
goready(gp, 3)
}
}
|
当 Channel 关闭时,我们必须让所有阻塞的接收方重新被调度,让所有的发送方也重新被调度,这时候 的实现先将 Goroutine 统一添加到一个列表中(需要锁),然后逐个地进行复始(不需要锁)。
https://golang.design/under-the-hood/zh-cn/part1basic/ch01basic/go/
Channel
Channel 主要有两种形式:
- 有缓存 Channel(buffered channel),使用
make(chan T, n)
创建
- 无缓存 Channel(unbuffered channel),使用
make(chan T)
创建
其中 T
为 Channel 传递数据的类型,n
为缓存的大小,这两种 Channel 的读写操作都非常简单:
1
2
3
4
5
6
7
8
|
// 创建有缓存 Channel
ch := make(chan interface{}, 10)
// 创建无缓存 Channel
ch := make(chan struct{})
// 发送
ch <- v
// 接受
v := <- ch
|
他们之间的本质区别在于其内存模型的差异,这种内存模型在 Channel 上体现为:
- 有缓存 Channel:
ch <- v
发生在 v <- ch
之前
- 有缓存 Channel:
close(ch)
发生在 v <- ch && v == isZero(v)
之前
- 无缓存 Channel:
v <- ch
发生在 ch <- v
之前
- 无缓存 Channel: 如果
len(ch) == C
,则从 Channel 中收到第 k 个值发生在 k+C 个值得发送完成之前
直观上我们很好理解他们之间的差异: 对于有缓存 Channel 而言,内部有一个缓冲队列,数据会优先进入缓冲队列,而后才被消费, 即向通道发送数据 ch <- v
发生在从通道接受数据 v <- ch
之前; 对于无缓存 Channel 而言,内部没有缓冲队列,即向通道发送数据 ch <- v
一旦出现, 通道接受数据 v <- ch
会立即执行, 因此从通道接受数据 v <- ch
发生在向通道发送数据 ch <- v
之前。 我们随后再根据实际实现来深入理解这一内存模型。
Go 语言还内建了 close()
函数来关闭一个 Channel:
但语言规范规定了一些要求:
-
读写 nil Channel 会永远阻塞,关闭 nil Channel 会导致 panic
-
关闭一个已关闭的 Channel 会导致 panic
-
向已经关闭的 Channel 发送数据会导致 panic
-
向已经关闭的 Channel 读取数据不会导致 panic,但读取的值为 Channel 传递的数据类型的零值,可以通过接受语句第二个返回值来检查 Channel 是否关闭且排空:
1
2
3
4
|
v, ok := <- ch
if !ok {
... // 如果是非缓冲 Channel ,说明已经关闭;如果是带缓冲 Channel ,说明已经关闭,且其内部缓冲区已经排空
}
|
How to Gracefully Close Channels -Go 101 https://go101.org/article/channel-closing.html
package main
import "fmt"
type T int
func IsClosed(ch <-chan T) bool {
select {
case <-ch:
return true
default:
}
return false
}
func main() {
c := make(chan T)
fmt.Println(IsClosed(c))
closed(T) 真则关闭了,假再执行操作时可能已经关闭了。
关闭原则:保证发送者唯一,让发送者关闭。
Check if a channel is closed without blocking the current goroutine
Assume it is guaranteed that no values were ever (and will be) sent to a channel, we can use the following code to (concurrently and safely) check whether or not the channel is already closed without blocking the current goroutine, where
T
the element type of the corresponding channel type.
func IsClosed(c chan T) bool {
select {
case <-c:
return true
default:
}
return false
}
The way to check if a channel is closed is used popularly in Go concurrent programming to check whether or not a notification has arrived. The notification will be sent by closing the channel in another goroutine.
安全地关闭通道
The Channel Closing Principle
One general principle of using Go channels is don't close a channel from the receiver side and don't close a channel if the channel has multiple concurrent senders. In other words, we should only close a channel in a sender goroutine if the sender is the only sender of the channel.
(Below, we will call the above principle as channel closing principle.)
Surely, this is not a universal principle to close channels. The universal principle is don't close (or send values to) closed channels. If we can guarantee that no goroutines will close and send values to a non-closed non-nil channel any more, then a goroutine can close the channel safely. However, making such guarantees by a receiver or by one of many senders of a channel usually needs much effort, and often makes code complicated. On the contrary, it is much easy to hold the channel closing principle mentioned above.