这一期我们简单讲一讲 Go 中 ​​channel​​ 的实现机制。

Go 中 ​​channel​​ 数据结构在 src/runtime/chan.go 中定义:

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​​ 的数据结构可以看出,它是由环形队列、类型信息、等待队列组成。


channel 实现机制_数据

环形队列

channel 实现机制_环形队列_02


首先, ​​channel​​ 里实现了一个环形队列作为其缓冲区,其长度由创建时指定,例如下面创建了一个可缓存 5 个元素的 channel:

channel 实现机制_数据_03

  • ​dataqsiz​​:指示了队列长度为 5 ,即可缓存 5 个元素;
  • ​buf​​:指向队列的内存;
  • ​qcount​​:表示队列中还有 3 个元素;
  • ​sendx​​:指示后续写入的数据存储的位置,取值[0, 5);
  • ​recvx​​:指示从该位置读取数据,取值[0, 5);

channel 实现机制_数据

等待队列

channel 实现机制_环形队列_02


其次,在 ​​channel​​ 中还有一个等待队列。

从 channel 读数据,如果 channel 缓冲区为空或者没有缓冲区,当前 goroutine 会被阻塞。向 channel 写数据,如果 channel 缓冲区已满或者没有缓冲区,当前 goroutine 也会被阻塞。

被阻塞的 goroutine 将会挂在 channel 的等待队列中:因读阻塞的 goroutine 会被向 channel 写入数据的 goroutine 唤醒;因写阻塞的 goroutine 会被从 channel 读数据的 goroutine 唤醒。

下面是一个没有缓冲区的 channel ,有几个 goroutine 被阻塞等待读数据:

channel 实现机制_写数据_06

一般情况下 ​​recvq​​​ 和 ​​sendq​​​ 至少有一个为空。但是同一个 goroutine 使用 ​​select​​ 语句向 channel 一边写数据,一边读数据时,两个都不为空。

一个 channel 只能传递一种类型的值,类型信息存储在 hchan 数据结构中。​​elemtype​​​ 代表类型,用于数据传递过程中的赋值;​​elemsize​​​ 代表类型大小,用于在 ​​buf​​ 中定位元素位置。


channel 实现机制_数据

创建 channel

channel 实现机制_环形队列_02


创建 channel 的过程实际上是初始化 ​​hchan​​​ 结构。其中类型信息和缓冲区长度由 ​​make​​​ 语句传入, ​​buf​​ 的大小则与元素大小和缓冲区长度共同决定。创建 channel 的源代码如下所示:

func makechan(t *chantype, size int) *hchan {
elem := t.elem

// compiler checks this but be safe.
if elem.size >= 1<<16 {
throw("makechan: invalid channel element type")
}
if hchanSize%maxAlign != 0 || elem.align > maxAlign {
throw("makechan: bad alignment")
}

mem, overflow := math.MulUintptr(elem.size, uintptr(size))
if overflow || mem > maxAlloc-hchanSize || size < 0 {
panic(plainError("makechan: size out of range"))
}

// 核心代码
var c *hchan
// 分配内存
switch {
case mem == 0:
// Queue or element size is zero.
c = (*hchan)(mallocgc(hchanSize, nil, true))
// Race detector uses this location for synchronization.
c.buf = c.raceaddr()
case elem.ptrdata == 0:
// Elements do not contain pointers.
// Allocate hchan and buf in one call.
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
// Elements contain pointers.
c = new(hchan)
c.buf = mallocgc(mem, elem, true)
}
// 元素类型大小
c.elemsize = uint16(elem.size)
// 元素类型
c.elemtype = elem
// 环形队列长度
c.dataqsiz = uint(size)
lockInit(&c.lock, lockRankHchan)

if debugChan {
print("makechan: chan=", c, "; elemsize=", elem.size, "; dataqsiz=", size, "\n")
}
return c
}

channel 实现机制_数据

向 channel 写数据

channel 实现机制_环形队列_02


向一个 channel 中写数据简单过程如下:

  1. 如果等待接收队列 ​​recvq​​ 不为空,说明缓冲区中没有数据或者没有缓冲区,此时直接从 ​​recvq​​ 取出 goroutine ,并把数据写入,最后把该 goroutine 唤醒,结束发送过程;
  2. 如果缓冲区中有空余位置,将数据写入缓冲区,结束发送过程;
  3. 如果缓冲区中没有空余位置,将待发送数据写入 goroutine ,将当前 goroutine 加入 ​​sendq​​ ,进入睡眠,等待被读 goroutine 唤醒。

简单流程图如下:

channel 实现机制_写数据_11

channel 实现机制_数据

从 channel 读数据

channel 实现机制_环形队列_02


从一个 channel 读数据简单过程如下:

  1. 如果等待发送队列 ​​sendq​​ 不为空,且没有缓冲区,直接从 ​​sendq​​ 中取出 goroutine ,把 goroutine 中数据读出,最后把 goroutine 唤醒,结束读取过程;
  2. 如果等待发送队列 ​​sendq​​ 不为空,此时说明缓冲区已满,从缓冲区中首部读出数据,把 goroutine 中数据写入缓冲区尾部,把 goroutine 唤醒,结束读取过程;
  3. 如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程;
  4. 将当前 goroutine 加入 ​​recvq​​ ,进入睡眠,等待被写 goroutine 唤醒。

简单流程图如下:

channel 实现机制_数据_14

channel 实现机制_数据

关闭 channel

channel 实现机制_环形队列_02


关闭 channel 时会把 ​​recvq​​​ 中的 goroutine 全部唤醒,本该写入 goroutine 的数据位置为 ​​nil​​​ 。把 ​​sendq​​ 中的 goroutine 全部唤醒,但这些 goroutine 会触发 panic。

除此之外,panic 出现的常见场景还有:

  1. 关闭值为 nil 的 channel;
  2. 关闭已经被关闭的 channel;
  3. 向已经关闭的 channel 写数据。