个发送的地址,recvx 表示下一个接收的地址。
recvq 表示等待接收的 sudog 列表,一个接收语句执行时,如果缓冲区没有数据而且当前没有别的发送者在等待,那么执行者 goroutine 会被挂起,并且将对应的 sudog 对象放到 recvq 中。
sendq 类似于 recvq,一个发送语句执行时,如果缓冲区已经满了,而且没有接收者在等待,那么执行者 goroutine 会被挂起,并且将对应的 sudog 放到 sendq 中。
closed 表示通道是否已经被关闭,0 代表没有被关闭,非 0 值代表已经被关闭。
lock 用于对 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 protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
}
type waitq struct {
first *sudog
last *sudog
}
4. 创建通道
当你在代码里面写了一句 c := make(chan int, 8)
时,编译器就会把它翻译成
t := typeof(chan int) // 编译器给你生成了 chan int 的类型描述信息,然后 t 指向这个类型描述信息
c := makechan(t, 8)
没错,makechan 就是创建通道的入口。它的目的就是构建 hchan 对象并返回。由于 hchan 在程序中始终以引用的形式存在,通过赋值或者是传参,它指向的都是同一个对象,所以 hchan 在标准库中都是以指针形式呈现给外部的。对于 makechan 的逻辑,这里分 3 种情况:
- 缓冲区所需大小为 0。对于这种情况,在为 hchan 分配内存时,只需要分配 sizeof(hchan) 大小的内存。这很好理解。
- 缓冲区所需大小不为 0,而且数据类型不包含指针。
我们先来理解下 不包含指针 这个东西,对于指针类型或者成员中有指针的类型,那就是包含指针的,否则就是不包含指针的。如下代码,A{}是不包含指针的,&A{}、B{}、&B{} 是包含指针的。
type A struct {
a int
b int
}
type B struct {
a *int
b *int
}
对于不包含指针的这种情况,分配一块连续内存容纳 hchan 和缓冲区对象。
- 缓冲区所需大小不为 0,而且数据类型包含指针。对于这种情况,分配两块内存,其中一块表示 hchan 对象,另外一块用来表示 buf。
下面是 makechan 的核心代码:
func makechan(t *chantype, size int) *hchan {
// ...
mem, overflow := math.MulUintptr(elem.size, uintptr(size))
var c *hchan
switch {
case mem == 0:
c = (*hchan)(mallocgc(hchanSize, nil, true))
case elem.kind&kindNoPointers != 0:
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
}
至于为什么要区分包含指针和不包含指针这两种情况,makechan 的注释给出了一段解释:
Hchan does not contain pointers interesting for GC when elements stored in buf do not contain pointers.
下面是我的猜想,如果不对,欢迎高人指正:
GC 不会知道 unsafe.Pointer 里面存储的是什么类型,因此如果实际元素类型里面包含指针,就要通过 mallocgc 将分配什么类型的数据告诉 gc,这样 gc 就不会回收这块内存中存储的指针所指向的内存。反之, buf 不包含指针,可以用一块大的内存来存储 hchan 对象和缓冲区,这样可以减轻 gc 压力。
5. 发送数据
向通道发送数据,runtime 中通过 chansend 实现,它的声明如下:
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool
参数 c 表示要向哪个 chan 发送数据, ep 表示要发送的数据的地址,block 表示是否需要阻塞, callerpc 表示调用地址。返回值 bool 表示数据是否成功发送。
block 是为了实现如下代码的语义:
c := make(chan int)
// ...
select {
case <-c:
// ...
default:
// ...
}
上面这段代码被编译成对 selectnbsend 的调用:
if selectnbsend(c, v) {
... foo
} else {
... bar
}
selectnbsend 的实现如下
func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {
return chansend(c, elem, false, getcallerpc()) // 非阻塞的发送
}
它与拥有多个 case 的 select 不同(多个 case 的 select 将在后文分析)。
chansend 按照下面的逻辑执行:
- 如果通道是空的,对于非阻塞的发送,直接返回 false。对于阻塞的通道,将 goroutine 挂起,并且永远不会返回
if c == nil {
if !block {
return false
}
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable")
}
- 非阻塞的情况下,如果通道没有关闭,而且当前没有接收者,缓冲区也已经满了或者没有缓冲区(即不可以发送数据)。那么直接返回 false
if !block && c.closed ==