前言
作为Go语言核心的数据结构之一,channel 是支撑Go高并发编程的关键组件。不管是业务开发还是个人学习,对channel的底层原理有所了解都是必要的。
channel的底层其实并不复杂,没有用到高深的结构或设计,如下图所示,主要是围绕着一个环形队列和两个双向链表展开。
相信你看完本篇文章,一定能够对channel的底层原理有更深的理解和体会。本文会从channel的底层数据结构 、初始化 、发送数据到channel 、从channel中接收数据 、关闭channel、通过select操作channel等常见操作展开,并会在文章末尾提出一些社区对于channel的思考和设计,例如:
-
如何实现无限缓存的channel?
-
如何实现Lock-Free的channel?
除此之外,本专栏还会更新对golang其他常见数据结构的源码解析,例如slice 和map等 ,感兴趣的同学可以持续关注。
专栏地址:[Golang源码解析]
如果有感兴趣的源码和问题可以随时后台交流。
chan数据结构
首先我们对channel的底层数据结构进行介绍,channel的底层数据结构是hchan struct,结构如下:
Go version:go 1.17
代码位置:src/runtime/chan.go
type hchan struct {
qcount uint // 队列中现存元素数量
dataqsiz uint // 环形队列容量
buf unsafe.Pointer // 环形队列头指针
elemsize uint16 // 元素大小
closed uint32 // channel是否关闭
elemtype *_type // 元素类型
sendx uint // 队列已发送位置索引
recvx uint // 队列已接收位置索引
recvq waitq // 由 recv 行为(也就是 <-ch)阻塞在 channel 上的 goroutine 队列
sendq waitq // 由 send 行为 (也就是 ch<-) 阻塞在 channel 上的 goroutine 队列
lock mutex //读写锁
}
type waitq struct {
first *sudog
last *sudog
}
划重点:
- recvq 是读操作阻塞在 channel 的 goroutine 列表
- sendq 是写操作阻塞在 channel 的 goroutine 列表
- recvq和sendq都是双向链表 ,FIFO
- buf使用ring buffer(环形缓存区),优点包括:
- 适合FIFO式的固定长度队列
- 可以预先分配固定大小 的数组
- 允许高效的内存访问 模式
- 所有的缓存区操作都是O(1),包括消费元素
- 本质上就是一个带有头尾指针的固定长度数组
- sudog 是等待goroutine以及数据的封装,是核心数据结构之一
创建channel
首先我们对channel的创建原理进行分析,常见的创建channel方式主要有如下两种:
ch1 := make(chan int)
ch2 := make(chan int,2)
我们可以通过go tool compile -N -l -S main.go命令将代码翻译为汇编指令,或者使用在线工具COMPILTER EXPLORER,以上代码的编译结果如下:go.godbolt.org/z/sM66YdWxs
查看部分带有CALL 指令的内容如下:
CALL runtime.makechan(SB)
CALL runtime.makechan(SB)
由以上编译结果,channel的创建函数对应的底层方法为runtime.makechan,makechan()方法主要分为两个部分:合法性验证和分配地址空间,接下来我们分别对其进行详细介绍:
代码位置:src**/runtime/chan.go**
2.1 合法性验证
合法性验证部分包括以下步骤:
- 数据类型大小验证,大于1<<16时异常
- 内存对齐(降低寻址次数),大于最大内存(8字节数)时异常
- 传入的size大于堆可分配的最大内存时异常
对应代码如下:
func makechan(t *chantype, size int) *hchan {
......
// 数据类型大小,大于1<<16时异常
if elem.size >= 1<<16 {
throw("makechan: invalid channel element type")
}
// 内存对齐(降低寻址次数),大于最大内存(8字节数)时异常
if hchanSize%maxAlign != 0 || elem.align > maxAlign {
throw("makechan: bad alignment")
}
// 传入的size,大于堆可分配的最大内存时异常
mem, overflow := math.MulUintptr(elem.size, uintptr(size))
if overflow || mem > maxAlloc-hchanSize || size < 0 {
panic(plainError("makechan: size out of range"))
}
......
}
2.2 分配地址空间
分配地址空间包括以下步骤:
- 根据 channel 中收发元素的类型和缓冲区的大小初始化 runtime.hchan 和缓冲区,分为三种情况:
- 如果不存在缓冲区,分配 hchan 结构体空间,即无缓存 channel
- 如果 channel 存储的类型不是指针类型,分配连续地址空间,包括 hchan 结构体 + 数据
- 默认情况包括指针,为 hchan 和 buf 单独分配数据地址空间
- 更新 hchan 结构体的数据,包括 elemsize、elemtype 和 dataqsiz
对应代码如下:
func makechan(t *chantype, size int) *hchan {
......
var c *hchan
switch {
case mem == 0:
// buf大小为0,因此只需要为hchan结构体分配空间
// hchanSize表示空hchan需要占用的字节
c = (*hchan)(mallocgc(hchanSize, nil, true))
// raceaddr内部实现为:return unsafe.Pointer(&c.buf)
c.buf = c.raceaddr()
case elem.ptrdata == 0:
// 队列中不存在指针,分配连续地址空间,大小为hchanSize+mem
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
// buf指针指向空hchan占用空间的末尾
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
// 队列包含指针类型
// 为buf单独开辟mem大小的空间,用来保存所有的数据
c = new(hchan)
c.buf = mallocgc(mem, elem, true)
}
// 更新 hchan 结构体的数据
c.elemsize = uint16(elem.size)
c.elemtype = elem
c.dataqsiz = uint(size)
lockInit(&c.lock, lockRankHchan)
......
}
发送数据到channel
常见的向channel中发送数据的操作如下:
ch <- 1
以上代码的编译结果如下:go.godbolt.org/z/vqYM8Pzdc
查看部分带有CALL 指令的内容如下:
CALL runtime.chansend1(SB)
由以上编译结果,发送数据到channel对应的底层方法是runtime.chansend1 ,由以下代码,chansend1只是调用了 runtime.chansend,调用时将 block 参数设置成 true,表示当前发送操作是阻塞的:
func chansend1(c *hchan, elem unsafe.Pointer) {
chansend(c, elem, true, getcallerpc())//阻塞
}
接下来我们对chansend函数进行详细介绍:
代码位置:src/runtime/chan.go
chansend函数主要可以归纳为四部分:
- 异常检查: 检查channel是否符合接收send请求的状态
- 同步发送:当存在等待的接收者时,也就是在 recvq 可以获得 waitq,通过 send 方法直接将数据发送给等待的接收者
- 异步发送:如果没有等待的goroutine,且缓冲区存在空余空间时,将发送的数据写入 channel 的缓冲区
- 阻塞发送:如果没有等待接收的goroutine且环形队列中也没有数据,则阻塞该goroutine等待其他goroutine从 channel 接收数据,将goroutine和数据打包成sudog存入sendq
4.1 异常检查
chansend()首先对channel进行异常检查:
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
if c == nil {//判断channal是否为nil
if !block {
return false
}
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)//休眠
throw("unreachable")
}
if debugChan {
print("chansend: chan=", c, "\n")
}
if raceenabled {
racereadpc(c.raceaddr(), callerpc, funcPC(chansend))
}
if !block && c.closed == 0 && full(c) {
//full为ture的两种情况1)无缓存通道,recvq为空 2)缓存通道,但是buffer已满
return false
}
......
}
异常检查主要包括:
- 首先判断channel是否为nil,向一个nil的channel发送数据会发生阻塞。如果是非阻塞模式直接返回false,否则调用gopark,引发以 waitReasonChanSendNilChan 为原因的休眠,并抛出 unreachable 的 fatal error。
- 当channel不为nil,此时检查channel是否做好接收发送操作的准备,即!block && c.closed == 0 && full(c),
- full(c)的失败场景包括
- 无缓冲区且recvq为空
- 有缓冲区且buf已满
func full(c *hchan) bool {
if c.dataqsiz == 0 {
// 无缓冲区channel并且recvq为空
return c.recvq.first == nil
}
// buf已满
return c.qcount == c.dataqsiz
}
异常检查之后,就是发送的核心逻辑 :
4.2 同步发送
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
......
lock(&c.lock)
if c.closed != 0 {//再次检查channel是否关闭,向已关闭的chan发送元素会引起panic
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
if sg := c.recvq.dequeue(); sg != nil {//取出第一个非空并且未被选择过的的sudog
// Found a waiting receiver. We pass the value we want to send
// directly to the receiver, bypassing the channel buffer (if any).
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true
}
......
}
在发送之前,先上锁,保证线程安全。并再一次检查 channel 是否关闭,如果关闭则抛出 panic。
如果有等待的接受者,也就是recvq队列中有waitq,通过dequeue() 取出头部第一个非空的 sudog,调用 send() 函数直接将数据拷贝到给等待接受的waitq:
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
......
if sg.elem != nil {
sendDirect(c.elemtype, sg, ep)
sg.elem = nil
}
gp := sg.g
unlockf()
gp.param = unsafe.Pointer(sg)
sg.success = true
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
goready(gp, skip+1)
}
send()函数主要包含两个工作:
- 调用 sendDirect() 将数据拷贝到接收变量的内存地址上
- 调用 goready() 将等待接收的阻塞 goroutine 的状态从 Gwaiting 或者 Gscanwaiting 改变成 Grunnable。下一轮调度时会唤醒这个接收的 goroutine。
4.3 异步发送
如果创建的channel为带缓冲区channel,接收者队列为空时,此时判断缓冲区是否已满,如果缓冲区未满,则进入异步发送逻辑,即将待接收的数据放入缓冲区中,代码如下:
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
......
if c.qcount < c.dataqsiz {// 缓冲区未满
qp := chanbuf(c, c.sendx)//获取缓存区index地址
if raceenabled {
racenotify(c, c.sendx, nil)
}
typedmemmove(c.elemtype, qp, ep)//数据写入buffer
c.sendx++
if c.sendx == c.dataqsiz {
c.sendx = 0
}
c.qcount++
unlock(&c.lock)
return true
}
......
}
4.4 阻塞发送
如果没有等待的接受者,且缓存区已满或无缓存channel,则进入阻塞发送 逻辑:
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
......
gp := getg()
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
mysg.elem = ep
mysg.waitlink = nil
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.waiting = mysg
gp.param = nil
c.sendq.enqueue(mysg)
// 把 goroutine 相关的线索结构入队,等待条件满足的唤醒;
atomic.Store8(&gp.parkingOnChan, 1)
// goroutine 切走,让出 cpu 执行权限;
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
KeepAlive(ep)
......
}
主要流程就是将goroutine休眠与数据等封装为sudog入sendq队列,细节如下:
- getg 获取发送数据的 goroutine,用于绑定给一个sudog
- acquireSudog 获取 sudog 结构,设置好 sudog 要发送的数据和状态。比如发送的 channel、是否在 select 中和待发送数据的内存地址等等。
- 调用 c.sendq.enqueue 方法将配置好的 sudog 加入待发送的等待队列,并设置到当前 goroutine 的 waiting上,表示 goroutine 正在等待该 sudog 准备就绪
- gopark 将当前的 goroutine 休眠等待唤醒
- KeepAlive() 确保发送的值保持活动状态,直到接收者将其复制出来。
在goroutine被调度器唤醒后会将一些属性置零并且释放 sudog 结构体,完成阻塞发送。代码如下:
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
......
// someone woke us up.
if mysg != gp.waiting {
throw("G waiting list is corrupted")
}
gp.waiting = nil
gp.activeStackChans = false
closed := !mysg.success
gp.param = nil
if mysg.releasetime > 0 {
blockevent(mysg.releasetime-t0, 2)
}
mysg.c = nil
releaseSudog(mysg)
if closed {
if c.closed == 0 {
throw("chansend: spurious wakeup")
}
panic(plainError("send on closed channel"))
}
return true
}
从channel中读取数据
Go 语言中可以使用两种不同的方式去接收 channel 中的数据:
i <- ch
i, ok <- ch
以上代码的编译结果如下:go.godbolt.org/z/4j3q9sWz6
查看部分带有CALL 指令的内容如下:
CALL runtime.chanrecv1(SB)
CALL runtime.chanrecv2(SB)
虽然不同的接收方式会被转换成 runtime.chanrecv1 和** runtime.chanrecv2 两种不同函数的调用,但是这两个函数最终还是会调用 runtime.chanrecv,此处也是阻塞**调用。
func chanrecv1(c *hchan, elem unsafe.Pointer) {
chanrecv(c, elem, true)
}
func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
_, received = chanrecv(c, elem, true)
return
}
接下来我们对chanrecv函数进行详细介绍:
代码位置:src/runtime/chan.go
chanrecv函数与chansend函数类似,同样可以归纳为四部分:
- 异常检查: 检查channel是否符合接收recv操作的状态
-
同步接收: 当存在等待的发送者时,也就是在 sendq 可以获得 waitq,此时对应两种情况:
- 无缓冲区channel,此时直接从发送方接收数据
- 有缓冲区channel,此时将缓冲区(buf)头部数据拷贝到接收者内存空间,并将sendq头部goroutine中数据拷贝到buf中
- 异步接收: 如果没有等待的goroutine,且buf中存在数据时,直接从buf中接收数据
- 阻塞接收: 如果没有等待发送的goroutine且buf中也没有数据,则阻塞该goroutine等待其他 goroutine向channel 发送数据,将goroutine和数据打包成sudog存入recvq
5.1 异常检查
chanrecv()首先对channel进行异常检查
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
if debugChan {
print("chanrecv: chan=", c, "\n")
}
if c == nil {
if !block {
return
}
gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
throw("unreachable")
}
if !block && empty(c) {
if atomic.Load(&c.closed) == 0 {
return
}
if empty(c) {
// channel不可逆的关闭并且为空
if raceenabled {
raceacquire(c.raceaddr())
}
if ep != nil {
typedmemclr(c.elemtype, ep)
}
return true, false
}
}
......
}
异常检查主要包括:
- 首先判断channel是否为nil,如果是非阻塞模式直接返回,否则调用gopark挂起goroutine。
- 接下来对channel 进行快速失败检查,检测 channel 是否已经准备好接收recv请求。
- empty()函数主要在两种情况下为true:
- 无缓冲区且sendq内没有等待发送的goroutine
- 有缓冲区且buf为空
empty()代码如下:
func empty(c *hchan) bool {
if c.dataqsiz == 0 {
return atomic.Loadp(unsafe.Pointer(&c.sendq.first)) == nil
}
return atomic.Loaduint(&c.qcount) == 0
}
异常检查之后,就是接收的核心逻辑 :
5.2 同步接收
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
......
lock(&c.lock)
if c.closed != 0 && c.qcount == 0 {
if raceenabled {
raceacquire(c.raceaddr())
}
unlock(&c.lock)
if ep != nil {
//内存处理
typedmemclr(c.elemtype, ep)
}
return true, false
}
if sg := c.sendq.dequeue(); sg != nil {
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true, true
}
......
}
接收之前先加锁,保证线程安全。再次对channel的状态(关闭且为空)进行验证,如不符合接收recv请求的状态,直接返回。
如果sendq中有等待的发送者 ,此时调用**recv()**函数完成同步接收。
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)
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
//将阻塞的发送方头节点goroutine唤醒
goready(gp, skip+1)
}
recv()方法主要包含以下步骤:
- 判断channel的缓存区大小
- 如果缓存区为0,此时直接从sendq中取出队头goroutine,直接从发送方接收值。
- 如果缓存区不为0,说明此时缓冲区已满,此时发生两次拷贝
- 首先将缓存区的数据拷贝到接收者目标地址
- 之后将发送队列sendq头结点的数据拷贝到缓存区位置
- 调用 goready 将等待接收数据的 goroutine 标记成可运行状态 Grunnable,并把该 goroutine 放到发送方所在处理器的 runnext 上,等待处理器唤醒。
5.3 异步接收
如果sendq为空且channel的缓冲区不为空时,则从缓冲区中接收数据,相应数据从buf中出队
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
......
if c.qcount > 0 {
// 直接从缓冲区接收数据
qp := chanbuf(c, c.recvx)
if raceenabled {
racenotify(c, c.recvx, nil)
}
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
}
......
}
5.4 阻塞接收
如果sendq中没有待发送的goroutine,且缓冲区为空,则进入阻塞发送逻辑:
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
......
if !block {
unlock(&c.lock)
return false, false
}
// no sender available: block on this channel.
gp := getg()
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
mysg.elem = ep
mysg.waitlink = nil
gp.waiting = mysg
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.param = nil
c.recvq.enqueue(mysg)
atomic.Store8(&gp.parkingOnChan, 1)
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)
.....
}
主要流程就是将goroutine休眠与数据等封装为sudog入recvq队列,细节如下:
- getg 获取发送数据的 goroutine
- acquireSudog 获取 sudog 结构
- 将创建并初始化的 sudog 加入recvq,并设置到当前 goroutine 的 waiting上,表示 goroutine 正在等待该 sudog 准备就绪
- gopark 将当前的 goroutine 陷入沉睡等待唤醒
被调度器唤醒后完成阻塞接收,之后进行参数检查,解除channel的绑定并释放sudog,代码如下:
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
......
// someone woke us up
if mysg != gp.waiting {
throw("G waiting list is corrupted")
}
gp.waiting = nil
gp.activeStackChans = false
if mysg.releasetime > 0 {
blockevent(mysg.releasetime-t0, 2)
}
success := mysg.success
gp.param = nil
mysg.c = nil
releaseSudog(mysg)
return true, success
}
关闭channel
关闭channel的操作如下:
close(ch)
以上代码的编译结果如下:go.godbolt.org/z/v51sf4TPc…
与close相关的CALL操作如下:
CALL runtime.closechan(SB)
接下来我们对closechan函数进行详细介绍:
代码位置:src/runtime/chan.go
closechan()函数的主要逻辑可以分为三部分:
- 异常检查 :首先对异常进行检查,判断channel是否符合close的状态
- 释放sudog :分别从recvq和sendq中回收等待的接收者和发送者,加入glist(待清除队列)中
- 调度goroutine :为glist中的所有阻塞goroutine触发调度
6.1 异常检查
closechan()首先对channel进行异常检查:
func closechan(c *hchan) {
if c == nil {
panic(plainError("close of nil channel"))
}
lock(&c.lock)
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("close of closed channel"))
}
c.closed = 1
......
}
- 判断channel是否是nil或者已经关闭
- 关闭nil channel或者已经关闭的channel都会引发panic
- 将channel状态标记为close
6.2 释放sudog
异常检查之后释放所有阻塞在sendq和recvq中的sudog:
func closechan(c *hchan) {
......
var glist gList
// release all readers
for {
sg := c.recvq.dequeue()
if sg == nil {
break
}
if sg.elem != nil {
typedmemclr(c.elemtype, sg.elem)
sg.elem = nil
}
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
gp := sg.g
gp.param = unsafe.Pointer(sg)
sg.success = false
if raceenabled {
raceacquireg(gp, c.raceaddr())
}
glist.push(gp)
}
// release all writers (they will panic)
for {
sg := c.sendq.dequeue()
if sg == nil {
break
}
sg.elem = nil
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
gp := sg.g
gp.param = unsafe.Pointer(sg)
sg.success = false
if raceenabled {
raceacquireg(gp, c.raceaddr())
}
glist.push(gp)
}
unlock(&c.lock)
......
}
- 回收接收者和发送者的sodug,并将其加入到glist中
6.3 调度goroutine
之后调用goready()为glist中所有goroutine触发调度,状态从 _Gwaiting 设置为 _Grunnable 状态,等待调度器的调度。
func closechan(c *hchan) {
......
// Ready all Gs now that we've dropped the channel lock.
for !glist.empty() {
gp := glist.pop()
gp.schedlink = 0
goready(gp, 3)
}
select
golang 中的 select 语句的实现,在 runtime/select.go 文件中,本篇文章并不打算探讨 select 的实现。
我们主要关注通过select操作channel时channel的状态。
7.1 向channel中发送数据
select {
case c <- x:
...
default:
...
}
会被编译为:
if selectnbsend(c, v) {
...
} else {
...
}
对应 selectnbsend 函数如下:
func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {
return chansend(c, elem, false, getcallerpc(unsafe.Pointer(&c)))
}
7.2 从channel中接收数据
select {
case v = <-c:
...
default:
...
}
会被编译为:
if selectnbrecv(&v, c) {
...
} else {
...
}
对应 selectnbrecv 函数如下:
func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected bool) {
selected, _ = chanrecv(c, elem, false)
return
}
另一种接收数据的方式:
select {
case v, ok = <-c:
...
default:
...
}
会被编译为:
if c != nil && selectnbrecv2(&v, &ok, c) {
...
} else {
...
}
对应 selectnbrecv2 函数如下:
func selectnbrecv2(elem unsafe.Pointer, received *bool, c *hchan) (selected bool) {
// TODO(khr): just return 2 values from this function, now that it is in Go.
selected, *received = chanrecv(c, elem, false)
return
}
7.3 总结
我们能看到,channel与select语句结合使用时,底层调用的还是chansend和chanrecv函数。
区别就在于:当与select语句结合使用时,是非阻塞调用,而不与select结合使用时,一般是阻塞调用。
总结
以上,我们完成了对channel数据结构,发送数据到channel,从channel中接收数据,关闭channel等常见操作的源码解析。
channel的操作并不复杂,结构上围绕着环形缓存和sendq、recvq展开,流程上围绕着上锁/解锁,阻塞/非阻塞,缓冲/非缓冲,缓存入队出队,sudog入队出队,协程休眠/唤醒等操作展开。
有兴趣的同学还可以思考一下社区比较受关注的问题:
-
如何设计无锁的channel
-
如何设计无限缓存的channel
本文正在参加技术专题18期-聊聊Go语言框架