在面试的时候,面试官:“有看过channel的源码吗?”(懵)。心想下去我一定要整理一下channel的源码分析。
阅读源码第一个问题:channel作为关键字,内置到语言中,怎么样找到channel的源码地址呢?
找到channel源码
go——“自带干电池”,那么我们就从干电池中寻找工具:`go tool objdump`编写一个简单的代码:
package main
import (
"fmt"
"time"
)
/**
@file:
@author: levi.Tang
@time: 2024/10/22 18:01
@description:
**/
func goRoutineA(a <-chan int) {
val := <-a
fmt.Println("goRoutineA received the data", val)
}
func main() {
ch := make(chan int)
go goRoutineA(ch)
ch <- 20
time.Sleep(time.Second * 1)
}
编译后, 通过objdump工具,获取程序中调用相关函数:
go tool objdump -s "main\.main" ./sample-1 |grep CALL
main.go:20 0x480c57 e80449f8ff CALL runtime.makechan(SB)
main.go:21 0x480c68 e893caf8ff CALL runtime.newobject(SB)
main.go:21 0x480c89 e85219feff CALL runtime.gcWriteBarrier1(SB)
main.go:21 0x480c9a e801e8fbff CALL runtime.newproc(SB)
main.go:22 0x480ca4 e857eafdff CALL time.Sleep(SB)
main.go:19 0x480caf e8ccfbfdff CALL runtime.morestack_noctxt.abi0(SB)
main.go:21 0x480ce0 e89bfeffff CALL main.goRoutineA(SB)
main.go:21 0x480ceb e810fbfdff CALL runtime.morestack.abi0(SB)
其中-s 表明:只输出 main.main 函数的汇编代码。这里的 's' 参数后面可以跟正则表达式,来匹配你想要反汇编的符号(函数、全局变量等)。
从输出来分析:
20行:在创建channel的时候,调用了runtime.makechan函数。
创建channel
```go // make(chan int, size)func makechan(t *chantype, size int) *hchan { elem := t.Elem // ...省略代码 mem, overflow := math.MulUintptr(elem.Size_, uintptr(size)) // ... 省略代码 var c *hchan switch { case mem == 0: // 队列或元素为空的时候, 进入该分支 c = (*hchan)(mallocgc(hchanSize, nil, true)) // 竞争检测器使用此位置进行同步。 c.buf = c.raceaddr() case elem.PtrBytes == 0: // 元素不包含指针类型 // 在一次调用中分配 hchan 和 buf。 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)
}
按照上面举例来看,会进入到#1个分支进行创建hchan对象:
math.MulUintptr函数:返回a*b以及是否溢出的判断,根据下面断点的输出,mem == 0;

可知size为0的时候,是make的是否不传入size,那什么时候元素大小为0呢?测试下,我使用了struct{} 作为参数:
```go
func emptyRoutineB(b <-chan struct{}) {
<-b
}
func main() {
ch := make(chan struct{}, 4)
go emptyRoutineB(ch)
time.Sleep(time.Second * 1)
}
断点显示:
最终channel的底层对象为:hchan。
hchan
字段含义如下:type hchan struct {
qcount uint // 当前channel剩余未被取走的对象, 可使用len进行获取
dataqsiz uint // 循环队列的大小, 即channel创建的size
buf unsafe.Pointer // 实际存储数据的循环队列
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // 缓存区待发送的索引
recvx uint // 缓存区待接收的索引
recvq waitq // 等待接受的队列
sendq waitq // 等待发送的队列
// 锁保护 hchan 中的所有字段,以及在此通道上阻塞的 sudogs 中的几个字段。
// 持有此锁时,请勿更改另一个 G 的状态(尤其是不要准备好一个 G),
// 因为这会导致堆栈收缩的死锁。
lock mutex
}
在结构体中,使用了多种数据结构类型:
buf: 使用循环队列,保存缓存对象,并通过sendx, recvx进行索引。
recvq, sendq: 使用双向链表,保存正在等待的goroutine。
sudog
在waitq双向链表中,保存的结构体为:sudog;//sudog(伪-g)表示等待列表中的一个 g,例如在一个通道上发送/接收。
// 一个 g 可能在多个等待列表上,因此一个 g 可能有多个 sudog;
// 多个 g 可能在同一个同步对象上等待,因此一个对象可能有多个 sudog。
// 使用 acquireSudog 和 releaseSudog 可以分配和释放它们。
type sudog struct {
// goroutine
g *g
next *sudog
prev *sudog
elem unsafe.Pointer // data element (may point to stack)
// 省略
c *hchan // channel
}
示例
通过示例来查看运行过程,hchan的内存视图:package main
import (
"fmt"
"time"
)
/**
@file:
@author: levi.Tang
@time: 2024/10/22 18:01
@description:
**/
func goRoutineA(a <-chan int) {
val := <-a
fmt.Println("goRoutineA received the data", val)
}
func goRoutineB(b <-chan int) {
val := <-b
fmt.Println("goRoutineB received the data", val)
}
func main() {
ch := make(chan int)
go goRoutineA(ch)
go goRoutineB(ch)
time.Sleep(time.Second * 1)
ch <- 3 // 将断点打到这个位置
time.Sleep(time.Second * 2)
}
使用dlv,在30行加入断点,查看内存对象:
goRoutineA,goRoutineB 都阻塞等待channel中的数据,在recvq对象中可以看到first,last分别指向不同的内存地址。当执行到31行,可以看到:
队列中已经出队一个对象,并且在终端上,获取到数据的goroutine已经输出信息:
dlv debug --headless --listen=:2345 --api-version=2 --accept-multiclient
API server listening at: [::]:2345
2024-10-24T15:53:16+08:00 warning layer=rpc Listening for remote connections (connections are not authenticated nor encrypted)
goRoutineA received the data 3
发送数据
将示例进行反汇编输出,获取发送channel的函数:`runtime.chansend1`func chansend1(c *hchan, elem unsafe.Pointer) {
chansend(c, elem, true, getcallerpc())
}
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool
底层还是调用chansend函数,代码有点长,分开进行解析;
向nil发送数据
当channel == nil时: if c == nil {
if !block {
return false
}
gopark(nil, nil, waitReasonChanSendNilChan, traceBlockForever, 2)
throw("unreachable")
}
当前goroutine将会调用gopark阻塞;
向close发送数据
```go lock(&c.lock)if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
直接崩溃。
<h2 id="yW1V7">recvq中存在等待者</h2>
```go
if sg := c.recvq.dequeue(); sg != nil {
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true
}
发现一个正在等待的接收器。我们将要发送的值直接传递给接收者,绕过通道缓冲区(如果有)。
send函数
```go 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) }// 在无缓冲或空缓冲通道上的发送和接收是一个运行中的程序向另一个运行中的程序的堆栈写入的唯一操作。 // GC 假定堆栈写入只发生在运行中的程序,而且只由该程序执行。 // typedmemmove 会调用 bulkBarrierPreWrite, // 但目标字节并不在堆中,因此无济于事。我们安排调用 memmove 和 typeBitsBulkBarrier。 func sendDirect(t *type, sg *sudog, src unsafe.Pointer) { // src 位于我们的栈中,dst 是另一个栈中的槽。一旦我们从 // sg 中读出 sg.elem,如果目标堆栈被复制(收缩), // 它将不再更新。因此要确保在读取和使用之间不会出现抢占点。 lock dst := sg.elem typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.Size) // 不需要 cgo 写入障碍检查,因为 dst 始终是 // Go memory. memmove(dst, src, t.Size_) }
从上面发现, send操作实际上是当前goroutine直接将参数放到目标goroutine的堆栈中。最好在调用`goready`将阻塞的goroutine的状态设置为`_Grunnable` ,这样goroutine调度器将捕获其进行运行。
<h2 id="CfNXY">存放到缓冲中</h2>
```go
// 当容量大于剩余未消费的数量时
if c.qcount < c.dataqsiz {
// 通道缓冲区中有可用空间。将要发送的元素排入队列。
qp := chanbuf(c, c.sendx)
typedmemmove(c.elemtype, qp, ep)
c.sendx++ // 发送索引+1
if c.sendx == c.dataqsiz {
c.sendx = 0
}
c.qcount++
unlock(&c.lock)
return true
}
通过比较 qcount 和 dataqsiz,确定 hchan.buf 是否有可用空间。将 ep 指针指向的区域复制到要发送的环形缓冲区,并调整 sendx 和 qcount,以此等待元素的发送。
这里有个比较有趣的地方:当sendx 已经与 缓存队列一样大的时候,会将其设置为 0(防止其溢出)。
然后直接返回,不会阻塞当前goroutine。
放入到sendq中
```go gp := getg() mysg := acquireSudog() mysg.releasetime = 0 if t0 != 0 { mysg.releasetime = -1 } // No stack splits between assigning elem and enqueuing mysg // on gp.waiting where copystack can find it. 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)gp.parkingOnChan.Store(true) gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceBlockChanSend, 2)
这么一长串总共做了以下几个事:
1. 获取当前发送数据的goroutine;
2. 创建一个sudog对象,将goroutine以及发送数据保存在sudog中;
3. 将当前的sudog插入到channel的发送队列中;
4. 向试图缩小堆栈的人发出信号,表明gourinte即将在通道上阻塞住。
5. 阻塞goroutine。
**在发送过程中:**
1. 锁住当前channel,不允许其他goroutine进行操作;
2. 尝试从recvq取出一个等待的goroutine, 然后将要写入的元素直接交给gorouine(直接写对方的堆栈)
3. 如果recvq为空,判断缓存区是否可用。如果可用,将当前的数据**拷贝**到缓存区中。(`<font style="color:rgb(36, 36, 36);">typedmemmove</font>`从 src.拷贝一个 t 类型的值到 dst)
4. 如果缓冲区已满,则**要写入的元素会保存在当前执行的 goroutine 结构中**,并且当前 goroutine 会在 sendq 处排队,并从运行时转成暂停。
> 请记住,在channel上所有数据的传输,都是通过拷贝值完成。
>
<h1 id="QTav5">接受数据</h1>
同样找到接受channel的函数:`runtime.chanrecv1`
```go
func chanrecv1(c *hchan, elem unsafe.Pointer) {
chanrecv(c, elem, true)
}
// chanrecv 在信道 c 上接收数据,并将接收到的数据写入 ep。
// 如果 block == false 且没有可用的元素,则返回(false,false)。
// 否则,如果 c 已关闭,则对 *ep 进行清零并返回(true, false)。
// 否则,在 *ep 中填入一个元素并返回(true, true)。非零的 *ep 必须指向堆或调用者的堆栈。
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool)
向nil接受数据
```go if c == nil { if !block { return } gopark(nil, nil, waitReasonChanReceiveNilChan, traceBlockForever, 2) throw("unreachable") } ```与发送一样,将自己阻塞了。
向close接受数据
```go lock(&c.lock) // 加锁 // channel已经关闭 if c.closed != 0 { // 如果没有可读数据 if c.qcount == 0 { // 解锁 unlock(&c.lock) if ep != nil { typedmemclr(c.elemtype, ep) } return true, false } // The channel has been closed, but the channel's buffer have data. } ```sendq中存在等待者
```go // Just found waiting sender with not closed. if sg := c.sendq.dequeue(); sg != nil { //发现了一位等待中的发送者。如果缓冲区大小为0,则直接从发送者那里接收值。 // 否则,从队列的头部接收值, // 并将发送者的值添加到队列的尾部(两者都映射到相同的缓冲区槽,因为队列已满)。 recv(c, sg, ep, func() { unlock(&c.lock) }, 3) return true, true } ```缓存区中存在数据
```go if c.qcount > 0 { // Receive directly from queue qp := chanbuf(c, c.recvx) 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
}
将数据放入拷贝到当前goroutine中,并更新q.count, recvx的值。最后解锁channel。
<h2 id="bPkqd">放入recvq中</h2>
```go
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)
gp.parkingOnChan.Store(true)
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceBlockChanRecv, 2)
同sender相似的处理逻辑。
关闭channel
同样找到函数:`runtime.closechan`
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
var glist gList
// 释放所有的接受者
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
glist.push(gp)
}
// 释放所有的发送者
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
glist.push(gp)
}
unlock(&c.lock)
// Ready all Gs now that we've dropped the channel lock.
for !glist.empty() {
gp := glist.pop()
gp.schedlink = 0
goready(gp, 3)
}
}
两个panic场景:
- 关闭为nil的channel
- 关闭已经关闭的channel
总结
在没看channel源码前,没有细想过几个场景为什么会出现panic是什么时候出现。看源码后:- channel的底层还是通过lock的方式将临界资源进行占用。
- channel通过sendq,recvq将本身与其他的goroutine进行连接,并通过gopark,goready来对goroutine的状态进行处理。
- channel数据的传输是通过值拷贝的方式进行,这里可以查看 medium.com/codeburst/d… 文章来测试。里面有几张图非常形象。
感悟:看源码其实没那么难,我们应该是抱有目的去查看主要的流程,而不是细枝末节的发散。每次发散到自己比较为难的时候,可以回过头再看梳理,自己是不是应该是了解这些细节。