【Go源码】channel实现
Channel作为Go CSP的重要组成部分
在传统的编程语言中,并发编程模型是基于线程和内存同步访问控制。
而CSP是一种新的并发编程模型,CSP的并发哲学:
Do not communicate by sharing memory; instead, share memory by communicating.
不要通过共享内存来通信,而要通过通信来实现内存共享。
Go 是第一个将 CSP 的这些思想引入,并且发扬光大的语言。
Go 的并发编程(CSP)的模型则用 goroutine 和 channel 来替代。
channel 提供了一种通信机制,通过它,一个 goroutine 可以向另一 goroutine 发送消息,channel内部有mutex 用于内存同步访问控制。
chan数据结构
src/runtime/chan.go:hchan定义了channel的数据结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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由队列、类型信息、goroutine等待队列组成,下面分别说明其原理。
buf 指向底层环形队列,只有缓冲型的 channel 才有。
sendx,recvx 均指向底层环形队列,表示当前可以发送和接收的元素位置索引值(相对于底层数组)。
sendq,recvq 分别表示被阻塞的 goroutine,这些 goroutine 由于尝试向channel发送数据 或从 channel 读取数据而被阻塞。
waitq 是 sudog 的一个双向链表,而 sudog 实际上是对 goroutine 的一个封装:
1 2 3 4 | type waitq struct { first *sudog last *sudog } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | type sudog struct { // The following fields are protected by the hchan.lock of the // channel this sudog is blocking on. shrinkstack depends on // this for sudogs involved in channel ops. g *g selectdone *uint32 // CAS to 1 to win select race (may point to stack) next *sudog prev *sudog elem unsafe.Pointer // data element (may point to stack) // The following fields are never accessed concurrently. // For channels, waitlink is only accessed by g. // For semaphores, all fields (including the ones above) // are only accessed when holding a semaRoot lock. acquiretime int64 releasetime int64 ticket uint32 parent *sudog // semaRoot binary tree waitlink *sudog // g.waiting list or semaRoot waittail *sudog // semaRoot c *hchan // channel } |
一个channel同时仅允许被一个goroutine读写,lock 用来保证每个读 channel 或写 channel 的操作都是原子的。
一个channel只能传递一种类型的值,类型信息存储在hchan数据结构中。
elemtype代表类型,用于数据传递过程中的赋值;
elemsize代表类型大小,用于在buf中定位元素位置。
环形队列
chan内部实现了一个环形队列作为其缓冲区,队列的长度是创建chan时指定的。
下图展示了一个可缓存6个整型类型元素的channel示意图:
dataqsiz指示了队列长度为6,即可缓存6个元素;
buf指向队列的内存,队列中还剩余两个元素;
qcount表示队列中还有两个元素;
sendx指示后续写入的数据存储的位置,取值[0, 6);
recvx指示从该位置读取数据, 取值[0, 6);
创建channel
使用make创建channel
// 无缓冲通道
ch1 := make(chan int)
// 有缓冲通道
ch2 := make(chan int, 10)
创建channel的过程实际上是初始化hchan结构。其中类型信息和缓冲区长度由make语句传入,buf的大小则与元素大小和缓冲区长度共同决定。
源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | func makechan(t *chantype, size int64) *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" ) } if size < 0 || int64(uintptr(size)) != size || (elem.size > 0 && uintptr(size) > (_MaxMem-hchanSize)/elem.size) { panic(plainError( "makechan: size out of range" )) } var c *hchan if elem.kind&kindNoPointers != 0 || size == 0 { // case 1: channel 不含有指针 // case 2: size == 0,即无缓冲 channel // Allocate memory in one call. // Hchan does not contain pointers interesting for GC in this case: // buf points into the same allocation, elemtype is persistent. // SudoG's are referenced from their owning thread so they can't be collected. // TODO(dvyukov,rlh): Rethink when collector can move allocated objects. // 在堆上分配连续的空间用作 channel c = (*hchan)(mallocgc(hchanSize+uintptr(size)*elem.size, nil, true)) if size > 0 && elem.size != 0 { c.buf = add(unsafe.Pointer(c), hchanSize) } else { // race detector uses this location for synchronization // Also prevents us from pointing beyond the allocation (see issue 9401). c.buf = unsafe.Pointer(c) } } else { // 有缓冲 channel 初始化 c = new(hchan) // 堆上分配 buf 内存 c.buf = newarray(elem, int(size)) } c.elemsize = uint16(elem.size) c.elemtype = elem c.dataqsiz = uint(size) if debugChan { print( "makechan: chan=" , c, "; elemsize=" , elem.size, "; elemalg=" , elem.alg, "; dataqsiz=" , size, "\n" ) } return c } |
channel特性
- 关闭一个未初始化(nil) 的 channel 会产生 panic;
- 重复关闭同一个 channel 会产生 panic;
- 向一个已关闭的 channel 中发送消息会产生 panic;
- 从已关闭的 channel 读取消息不会产生 panic,且能读出 channel 中还未被读取的消息,若消息均已读出,则会读到类型的零值。从一个已关闭的 channel 中读取消息永远不会阻塞,并且会返回一个为 false 的 ok-idiom,可以用它来判断 channel 是否关闭;
- 关闭 channel 会产生一个广播机制,所有向 channel 读取消息的 goroutine 都会收到消息。
- 从无缓存的 channel 中读取消息会阻塞,直到有 goroutine 向该 channel 中发送消息;
- 向无缓存的 channel 中发送消息也会阻塞,直到有 goroutine 从 channel 中读取消息。
- 有缓存的 channel 当缓存未满时,向 channel 中发送消息时不会阻塞,当缓存满时,发送操作将被阻塞,直到有其他 goroutine 从中读取消息;
- 有缓存的 channel 当消息不为空时,读取channel中消息不会出现阻塞,当 channel 为空时,读取操作会造成阻塞,直到有 goroutine 向 channel 中写入消息。
向channel写数据
向一个channel中写数据简单过程如下:
- 如果等待接收队列recvq不为空,说明缓冲区中没有数据或者没有缓冲区,此时直接从recvq取出G,并把数据写入,最后把该G唤醒,结束发送过程;
- 如果缓冲区中有空余位置,将数据写入缓冲区,结束发送过程;
- 如果缓冲区中没有空余位置,将待发送数据写入G,将当前G加入sendq,进入睡眠,等待被读goroutine唤醒;

源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | // entry point for c <- x from compiled code //go:nosplit func chansend1(c *hchan, elem unsafe.Pointer) { chansend(c, elem, true, getcallerpc(unsafe.Pointer(&c))) } /* * generic single channel send/recv * If block is not nil, * then the protocol will not * sleep but return if it could * not complete. * * sleep can wake up with g.param == nil * when a channel involved in the sleep has * been closed. it is easiest to loop and re-run * the operation; we'll see that it's now closed. */ func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool { //当 channel 未初始化或为 nil 时,向其中发送数据将会永久阻塞 if c == nil { if !block { return false } // gopark 会使当前 goroutine 休眠,并通过 unlockf 唤醒,但是此时传入的 unlockf 为 nil, 因此,goroutine 会一直休眠 gopark(nil, nil, "chan send (nil chan)" , traceEvGoStop, 2) throw( "unreachable" ) } if debugChan { print( "chansend: chan=" , c, "\n" ) } if raceenabled { racereadpc(unsafe.Pointer(c), callerpc, funcPC(chansend)) } // Fast path: check for failed non-blocking operation without acquiring the lock. // // After observing that the channel is not closed, we observe that the channel is // not ready for sending. Each of these observations is a single word-sized read // (first c.closed and second c.recvq.first or c.qcount depending on kind of channel). // Because a closed channel cannot transition from 'ready for sending' to // 'not ready for sending', even if the channel is closed between the two observations, // they imply a moment between the two when the channel was both not yet closed // and not ready for sending. We behave as if we observed the channel at that moment, // and report that the send cannot proceed. // // It is okay if the reads are reordered here: if we observe that the channel is not // ready for sending and then observe that it is not closed, that implies that the // channel wasn't closed during the first observation. if !block && c.closed == 0 && ((c.dataqsiz == 0 && c.recvq.first == nil) || (c.dataqsiz > 0 && c.qcount == c.dataqsiz)) { return false } var t0 int64 if blockprofilerate > 0 { t0 = cputicks() } // 获取同步锁 lock(&c.lock) //向已经关闭的 channel 发送消息会产生 panic if c.closed != 0 { unlock(&c.lock) panic(plainError( "send on closed channel" )) } // CASE1: 当有 goroutine 在 recv 队列上等待时,跳过缓存队列,将消息直接发给 reciever goroutine if sg := c.recvq.dequeue(); sg != nil { // 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 } // CASE2: 缓存队列未满,则将消息复制到缓存队列上 if c.qcount < c.dataqsiz { // Space is available in the channel buffer. Enqueue the element to send. qp := chanbuf(c, c.sendx) if raceenabled { raceacquire(qp) racerelease(qp) } typedmemmove(c.elemtype, qp, ep) c.sendx++ if c.sendx == c.dataqsiz { c.sendx = 0 } c.qcount++ unlock(&c.lock) return true } if !block { unlock(&c.lock) return false } // CASE3: 缓存队列已满,将goroutine 加入 send 队列 // 初始化 sudog // Block on the channel. Some receiver will complete our operation for us. 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.selectdone = nil mysg.c = c gp.waiting = mysg gp.param = nil // 加入sendq队列 c.sendq.enqueue(mysg) // 休眠 goparkunlock(&c.lock, "chan send" , traceEvGoBlockSend, 3) // 唤醒 goroutine // someone woke us up. if mysg != gp.waiting { throw( "G waiting list is corrupted" ) } gp.waiting = nil if gp.param == nil { if c.closed == 0 { throw( "chansend: spurious wakeup" ) } panic(plainError( "send on closed channel" )) } gp.param = nil if mysg.releasetime > 0 { blockevent(mysg.releasetime-t0, 2) } mysg.c = nil releaseSudog(mysg) return true } |
从channel读数据
向一个channel中写数据简单过程如下:
- 如果等待接收队列recvq不为空,说明缓冲区中没有数据或者没有缓冲区,此时直接从recvq取出G,并把数据写入,最后把该G唤醒,结束发送过程;
- 如果缓冲区中有空余位置,将数据写入缓冲区,结束发送过程;
- 如果缓冲区中没有空余位置,将待发送数据写入G,将当前G加入sendq,进入睡眠,等待被读goroutine唤醒;
简单流程图如下:
源码
接收操作有两种写法,一种带 "ok",反应 channel 是否关闭;一种不带 "ok",这种写法,当接收到相应类型的零值时无法知道是真实的发送者发送过来的值,还是 channel 被关闭后,返回给接收者的默认类型的零值。两种写法,都有各自的应用场景。
1 2 3 4 5 6 7 8 9 | // entry points for <- c from compiled code 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 } |
chanrecv1
函数处理不带 "ok" 的情形,chanrecv2
则通过返回 "received" 这个字段来反应 channel 是否被关闭。接收值则比较特殊,会“放到”参数 elem
所指向的地址了,这很像 C/C++ 里的写法。如果代码里忽略了接收值,这里的 elem 为 nil。
无论如何,最终转向了 chanrecv
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | // 位于 src/runtime/chan.go // chanrecv 函数接收 channel c 的元素并将其写入 ep 所指向的内存地址。 // 如果 ep 是 nil,说明忽略了接收值。 // 如果 block == false,即非阻塞型接收,在没有数据可接收的情况下,返回 (false, false) // 否则,如果 c 处于关闭状态,将 ep 指向的地址清零,返回 (true, false) // 否则,用返回值填充 ep 指向的内存地址。返回 (true, true) // 如果 ep 非空,则应该指向堆或者函数调用者的栈 func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) { // 省略 debug 内容 ………… // 如果是一个 nil 的 channel if c == nil { // 如果不阻塞,直接返回 (false, false) if !block { return } // 否则,接收一个 nil 的 channel,goroutine 挂起 gopark(nil, nil, "chan receive (nil chan)" , traceEvGoStop, 2) // 不会执行到这里 throw( "unreachable" ) } // 在非阻塞模式下,快速检测到失败,不用获取锁,快速返回 // 当我们观察到 channel 没准备好接收: // 1. 非缓冲型,等待发送列队 sendq 里没有 goroutine 在等待 // 2. 缓冲型,但 buf 里没有元素 // 之后,又观察到 closed == 0,即 channel 未关闭。 // 因为 channel 不可能被重复打开,所以前一个观测的时候 channel 也是未关闭的, // 因此在这种情况下可以直接宣布接收失败,返回 (false, false) if !block && (c.dataqsiz == 0 && c.sendq.first == nil || c.dataqsiz > 0 && atomic.Loaduint(&c.qcount) == 0) && atomic.Load(&c.closed) == 0 { return } var t0 int64 if blockprofilerate > 0 { t0 = cputicks() } // 加锁 lock(&c.lock) // channel 已关闭,并且循环数组 buf 里没有元素 // 这里可以处理非缓冲型关闭 和 缓冲型关闭但 buf 无元素的情况 // 也就是说即使是关闭状态,但在缓冲型的 channel, // buf 里有元素的情况下还能接收到元素 if c.closed != 0 && c.qcount == 0 { if raceenabled { raceacquire(unsafe.Pointer(c)) } // 解锁 unlock(&c.lock) if ep != nil { // 从一个已关闭的 channel 执行接收操作,且未忽略返回值 // 那么接收的值将是一个该类型的零值 // typedmemclr 根据类型清理相应地址的内存 typedmemclr(c.elemtype, ep) } // 从一个已关闭的 channel 接收,selected 会返回true return true, false } // 等待发送队列里有 goroutine 存在,说明 buf 是满的 // 这有可能是: // 1. 非缓冲型的 channel // 2. 缓冲型的 channel,但 buf 满了 // 针对 1,直接进行内存拷贝(从 sender goroutine -> receiver goroutine) // 针对 2,接收到循环数组头部的元素,并将发送者的元素放到循环数组尾部 if sg := c.sendq.dequeue(); sg != nil { // Found a waiting sender. If buffer is size 0, receive value // directly from sender. Otherwise, receive from head of queue // and add sender's value to the tail of the queue (both map to // the same buffer slot because the queue is full). recv(c, sg, ep, func () { unlock(&c.lock) }, 3) return true, true } // 缓冲型,buf 里有元素,可以正常接收 if c.qcount > 0 { // 直接从循环数组里找到要接收的元素 qp := chanbuf(c, c.recvx) // ………… // 代码里,没有忽略要接收的值,不是 "<- ch",而是 "val <- ch",ep 指向 val if ep != nil { typedmemmove(c.elemtype, ep, qp) } // 清理掉循环数组里相应位置的值 typedmemclr(c.elemtype, qp) // 接收游标向前移动 c.recvx++ // 接收游标归零 if c.recvx == c.dataqsiz { c.recvx = 0 } // buf 数组里的元素个数减 1 c.qcount-- // 解锁 unlock(&c.lock) return true, true } if !block { // 非阻塞接收,解锁。selected 返回 false,因为没有接收到值 unlock(&c.lock) return false, false } // 接下来就是要被阻塞的情况了 // 构造一个 sudog 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.selectdone = nil mysg.c = c gp.param = nil // 进入channel 的等待接收队列 c.recvq.enqueue(mysg) // 将当前 goroutine 挂起 goparkunlock(&c.lock, "chan receive" , traceEvGoBlockRecv, 3) // 被唤醒了,接着从这里继续执行一些扫尾工作 if mysg != gp.waiting { throw( "G waiting list is corrupted" ) } gp.waiting = nil if mysg.releasetime > 0 { blockevent(mysg.releasetime-t0, 2) } closed := gp.param == nil gp.param = nil mysg.c = nil releaseSudog(mysg) return true, !closed } |
关闭channel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | func closechan(c *hchan) { // 关闭一个 nil channel,panic if c == nil { panic(plainError( "close of nil channel" )) } // 上锁 lock(&c.lock) // 如果 channel 已经关闭 if c.closed != 0 { unlock(&c.lock) // panic panic(plainError( "close of closed channel" )) } // ………… // 修改关闭状态 c.closed = 1 var glist *g // 将 channel 所有等待接收队列的里 sudog 释放 for { // 从接收队列里出队一个 sudog sg := c.recvq.dequeue() // 出队完毕,跳出循环 if sg == nil { break } // 如果 elem 不为空,说明此 receiver 未忽略接收数据 // 给它赋一个相应类型的零值 if sg.elem != nil { typedmemclr(c.elemtype, sg.elem) sg.elem = nil } if sg.releasetime != 0 { sg.releasetime = cputicks() } // 取出 goroutine gp := sg.g gp.param = nil if raceenabled { raceacquireg(gp, unsafe.Pointer(c)) } // 相连,形成链表 gp.schedlink.set(glist) glist = gp } // 将 channel 等待发送队列里的 sudog 释放 // 如果存在,这些 goroutine 将会 panic for { // 从发送队列里出队一个 sudog sg := c.sendq.dequeue() if sg == nil { break } // 发送者会 panic sg.elem = nil if sg.releasetime != 0 { sg.releasetime = cputicks() } gp := sg.g gp.param = nil if raceenabled { raceacquireg(gp, unsafe.Pointer(c)) } // 形成链表 gp.schedlink.set(glist) glist = gp } // 解锁 unlock(&c.lock) // Ready all Gs now that we've dropped the channel lock. // 遍历链表 for glist != nil { // 取最后一个 gp := glist // 向前走一步,下一个唤醒的 g glist = glist.schedlink.ptr() gp.schedlink = 0 // 唤醒相应 goroutine goready(gp, 3) } } |
refer:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?