channel(通道)用于 goroutine(协程)之间的通信。它提供了一种在不同协程之间传递数据的机制。channel 是一种类型安全的、阻塞的、先进先出(FIFO)的数据结构,确保发送的数据按照发送的顺序接收。
Go 语言提倡通过通信来共享内存,而不是通过共享内存来通信,CSP(Communicating Sequential Process)并发模型,就是通过 goroutine 和 channel 来实现的。
1.底层实现
1.1 hchan
通过 var
声明或者 make
函数创建的 channel
变量是一个存储在函数栈帧上的指针,指向堆上的 hchan
结构体。
// src/runtime/chan.go
type hchan struct {
qcount uint // 循环数组中的元素数量
dataqsiz uint // 循环数组的长度
// channel 分为无缓冲和有缓冲两种。
// 对于有缓冲的 channel 存储数据,使用了 ring buffer(环形缓冲区)来缓存写入的数据,本质是循环数组。
// 为什么是循环数组?普通数组不行吗?普通数组容量固定,更适合指定的空间,弹出元素时,普通数组需要全部都前移。
buf unsafe.Pointer // 指向底层循环数组的指针(环形缓冲区)
elemsize uint16 // 元素的大小
closed uint32 // channel是否关闭的标志
elemtype *_type // channel中的元素类型
// 当下标超过数组容量后会回到第一个位置,所以需要有两个字段记录当前读和写的下标位置
sendx uint // 下一次写下标的位置
recvx uint // 下一次读下标的位置
// 尝试读/写 channel 被阻塞的 goroutine
recvq waitq // 读等待队列
sendq waitq // 写等待队列
lock mutex //互斥锁,保证读写 channel 时不存在并发竞争问题
}
1.2 sudog
等待队列是双向链表结构,每个节点是一个 sudog
结构体变量,记录哪个协程在等待,等待的是哪个 channel,等待发送/接收的数据在哪里。
// $GOROOT/src/runtime/chan.go
type waitq struct {
first *sudog
last *sudog
}
type sudog struct {
g *g
next *sudog
prev *sudog
elem unsafe.Pointer
...
c *hchan
}
2.执行基本操作时的底层操作
2.1 创建
当我们使用 make(chan T, cap)
来创建 channel 时,make
语法会在编译时转换为 makechan64
和 makechan
:
// $GOROOT/src/runtime/chan.go
func makechan64(t *chantype, size int64) *hchan {
// 确保将 size 转换为 int 类型后与原始值相等,否则引发 panic
if int64(int(size)) != size {
panic(plainError("makechan: size out of range"))
}
return makechan(t, int(size))
}
func makechan(t *chantype, size int) *hchan {
// 获取元素类型
elem := t.elem
// 编译器已经检查了这一点,但是为了安全起见再次进行检查
// 如果元素大小大于等于 1<<16(65536),则抛出错误
if elem.size >= 1<<16 {
throw("makechan: invalid channel element type")
}
// 检查 hchanSize 是否能被 maxAlign 整除,或者 elem.align 是否大于 maxAlign
if hchanSize%maxAlign != 0 || elem.align > maxAlign {
throw("makechan: bad alignment")
}
// 计算所需的内存大小
mem, overflow := math.MulUintptr(elem.size, uintptr(size))
// 检查是否溢出、所需内存是否超过 maxAlloc - hchanSize 或者 size 是否小于 0
if overflow || mem > maxAlloc-hchanSize || size < 0 {
panic(plainError("makechan: size out of range"))
}
// 创建 hchan 指针
var c *hchan
switch {
case mem == 0:
// 队列或元素大小为 0:分配 hchan 的内存
c = (*hchan)(mallocgc(hchanSize, nil, true))
// 设置 buf 用于与竞争检测器同步
c.buf = c.raceaddr()
case elem.ptrdata == 0:
// 元素不包含指针
// 一次性分配 hchan 和 buf 的内存
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
// 元素包含指针:创建 hchan,然后分配 buf 的内存
c = new(hchan)
c.buf = mallocgc(mem, elem, true)
}
// 设置 hchan 的属性
c.elemsize = uint16(elem.size)
c.elemtype = elem
c.dataqsiz