本文是对Golang channel的关键源码进行走读,并记录使用时容易犯的一些错误。示例可能来源于网络,对应的Golang版本是1.21.9,仅供个人学习使用。
使用语法和基本示例
1. 超时控制
func waitForStopOrTimeout(stopCh <-chan struct{
}, timeout time.Duration) <- chan struct{
} {
stopChWithTimeout := make(chan struct{
})
go func() {
select {
case <-stopCh: // 收到停止信号
case <-time.After(timeout): // 等待超时
}
close(stopChWithTimeout)
}()
return stopChWithTimeout
}
该函数返回一个管道,可用于在函数之间传递,但该管道会在指定时间后自动关闭。
2. 控制并发数
var token = make(chan int, 10) // 创建缓冲型channel, 容量为10
func main() {
for _, w := range work {
go func() {
token <- 1
w() // 执行任务,访问第三方动作在w()中
<-token
}()
}
}
- 如果把token <- 1放到func外层,就是控制系统goroutine的数量。
- channel控制子协程的方式,不如waitGroup、Context等更优雅。
数据结构
// channel 数据结构
type hchan struct {
qcount uint // 当前 channel 中存在多少个元素;
dataqsiz uint // 当前 channel 能存放的元素容量;
buf unsafe.Pointer // channel 中用于存放元素的环形缓冲区;
elemsize uint16 // channel 元素类型的大小;
closed uint32 // 标识 channel 是否关闭;
elemtype *_type // channel 元素类型;
sendx uint // 发送元素进入环形缓冲区的 index;
recvx uint // 接收元素所处的环形缓冲区的 index;
recvq waitq // 因接收而陷入阻塞的协程队列;
sendq waitq // 因发送而陷入阻塞的协程队列;
lock mutex // 用来确保每个读/写操作对 channel 的修改是原子的;
}
// waitq 是 sudog 的双向链表,用于存放因接收而陷入阻塞的协程;
type waitq struct {
first *sudog // 队列头部
last *sudog // 队列尾部
}
// sudog是对goroutine的封装
type sudog struct {
g *g
next *sudog // 队列中的下一个节点;
prev *sudog // 队列中的上一个节点;
elem unsafe.Pointer // 读取/写入 channel 的数据的容器;
acquiretime int64
releasetime int64
ticket uint32
// 标识当前协程是否处在 select 多路复用的流程中
// g.selectDone must be CAS'd to win the wake-up race.
isSelect bool
// success 表示跟 c通道通信的状态;
// true 代表 goroutine已激活,通道 c 中存在数据;
// false 代表 goroutine 已被唤醒,但通道 c 中不存在数据。
success bool
parent *sudog // semaRoot binary tree
waitlink *sudog // g.waiting list or semaRoot
waittail *sudog // semaRoot
c *hchan // 标识与当前 sudog 交互的 chan.
}
初始化
初始化的过程,主要是在堆上分配内存,并初始化channel的成员变量。
const (
maxAlign = 8
hchanSize = unsafe.Sizeof(hchan{
}) + uintptr(-int(unsafe.Sizeof(hchan{
}))&(maxAlign-1))
debugChan = false
)
func makechan(t *chantype, size int) *hchan {
elem := t.Elem
// 编译器代码越界检查
if elem.Size_ >= 1<<16 {
throw("makechan: invalid channel element type")
}
if hchanSize%maxAlign != 0 || elem.Align_ > maxAlign {
throw("makechan: bad alignment")
}
// 计算 channel 需要的内存大小,以及是否超出最大值
mem, overflow := math.MulUintptr(elem.Size_, uintptr(size))
if overflow || mem > maxAlloc-hchanSize || size < 0 {
panic(plainError("makechan: size out of range"))
}
// 根据类型,初始 channel,分为 无缓冲型、有缓冲元素为 struct 型、有缓冲元素为 pointer 型 channel;
var c *hchan
switch {
case mem == 0:
// Channel 容量或元素大小为0
// 1. 非缓冲型: buf没用,直接指向chan起始地址
c = (*hchan)(mallocgc(hchanSize, nil, true))
// Race detector uses this location for synchronization.
c.buf = c.raceaddr()
case elem.PtrBytes == 0:
// 缓冲的 struct 型: 一次性分配好 96 + mem 大小的空间,并且调整 chan 的 buf 指向 mem 的起始位置;
// (只进行一次内存分配操作)
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
// 有缓冲的 pointer 型,则分别申请 chan 和 buf 的空间,两者无需连续(两次内存分配操作)
c = new(hchan)
c.buf = mallocgc(mem, elem, true)
}
c