5. Channel
5. Channel
为什么使用管道而不使用共享内存:
- 避免协程竞争和数据冲突问题
- 降低开发难度,增加程序可读性
- 模块之间更容易解耦,增加扩展性和可维护性
5.1 Channel 底层数据结构
实现 Channel 大概需要如下几个部分:
- 如果没有协程接收,需要一个队列记录正在发送的协程
- 如果没有协程发送,需要一个记录接收数据协程的队列
- 需要一个缓冲区,缓冲发送的数据
查看 Go 语言 Channel 的底层数据结构:runtime/chan.go
type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex // 用于保护hchan中的字段,任何协程想要操作该结构体都需要获取这把锁
}
- 其中下面的这些字段组成了一个环形缓存区
qcount uint // 环形队列中元素的数量
dataqsiz uint // 环形缓冲区大小
buf unsafe.Pointer // 指向环形缓冲区中的元素
elemsize uint16 // 元素大小
elemtype *_type // 元素类型
- 环形缓存可以大幅度降低 GC 的开销
- 如果使用队列,不断新增/删除数据需要开辟/回收内存空间,这会造成 GC 压力大
- 如果使用环形缓存,永远占用同一片内存,有新的数据来的时候,直接放在下一个块上就可以了
发送和接收队列有下面四个属性组成
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters 等待接收队列
sendq waitq // list of send waiters 等待发送队列
-
这里没有使用 sema,而是使用链表实现发送队列和接收队列
-
查看 waitq 结构体,sudog 前面已经介绍过了,里面会存放一个协程,这是一个以 sudog 为节点的双向链表
type waitq struct {
first *sudog // 指向链表第一个成员
last *sudog // 指向链表最后一个成员
}
注意:hchan 结构体的 lock 是一个 Mutex,该锁并不是用来排队发送/接收数据的
- 这里的互斥锁用来保护 hchan 结构体的所有字段的,任何协程想要操作 hchan 结构体中的变量,就需要加 Mutex
- 就像前面讲解的员工结构体,如果想要修改员工结构体中的数据,就需要加锁
- Channel 本身并不是无锁的,他带有一个 Mutex,所以说 Channel 是一个无锁的并不完全正确
- Channel 快是因为我们只是在往 Channel 中塞数据的时候需要加锁,塞完数据后就可以解锁了,不需要一直加锁
补充:hchan 结构体中还有一个 closed 字段,0 表示开启,1 表示关闭
总结:Channel 是 go 的一等公民,大致结构体如下:

5.2 Channel 发送数据
在编译期间,会将 ch <- 操作转换为 runtime.chansend1() 方法,如下:
// entry point for c <- x from compiled code
//go:nosplit
func chansend1(c *hchan, elem unsafe.Pointer) {
chansend(c, elem, true, getcallerpc())
}
- chansend1 中调用 chansend 方法,查看 chansend 方法
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
if c == 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) {
return false
}
var t0 int64
if blockprofilerate > 0 {
t0 = cputicks()
}
lock(&c.lock)
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
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
}
if c.qcount < c.dataqsiz {
// Space is available in the channel buffer. Enqueue the element to send.
qp := chanbuf(c, c.sendx)
if raceenabled {
racenotify(c, c.sendx, nil)
}
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
}
// 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.isSelect = false
mysg.c = c
gp.waiting = mysg
gp.param = nil
c.sendq.enqueue(mysg)
atomic.Store8(&gp.parkingOnChan, 1)
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
KeepAlive(ep)
// 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 发送分为三种情形:
- 直接发送
- 放入缓存
- 休眠等待
5.2.1 直接发送
直接发送:
- 发送之前已经有 G 在休眠等待接收数据了
- 此时缓存肯定是空的,不用考虑缓存
- 将数据直接拷贝给 G 的接收变量,唤醒 G(不需要将数据存放到缓存区中)
直接发送实现步骤:
- 从接收队列中取出一个等待接收的 G
- 将数据直接拷贝到接收变量中
- 唤醒 G
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
...
lock(&c.lock) // 往channel中添加数据,需要修改hchan结构体,需要上锁
if c.closed != 0 { // 判断 channel 是否已经被关闭
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
// 1. 从等待队列中取一个协程,并判断拿出来的是否是nil,如果不是nil,表示有协程在等待接收数据
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).
// 2. sg不是nil表示有协程在等待,执行send方法,参数(channel, 等待接收的协程的sudog, s)
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true
}
...
}
func lock(l *mutex) {
lockWithRank(l, getLockRank(l))
}
- 查看 send 方法
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
...
// 1. sg 是真正等待的协程的包装,sg.elem 是协程存放接收的数据的地方
// 例如 i := <-c,就会将 i 这个变量的指针放在 elem 中(指向了要用哪一个变量来接收channel中的数据)
// 如果g中的数据不为空,执行如下逻辑
if sg.elem != nil {
// 执行 sendDirect 方法,参数(chan中数据的类型, 正在等待的协程sudog, 数据的指针)
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) // 数据接收完成后唤醒等待的协程,和goproc相反
}
- 查看 sendDirect 方法
// 参数(chan中数据的类型, 正在等待的协程sudog, 数据的指针)
func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) {
// src is on our stack, dst is a slot on another stack.
// Once we read sg.elem out of sg, it will no longer
// be updated if the destination's stack gets copied (shrunk).
// So make sure that no preemption points can happen between read & use.
dst := sg.elem // 目的地的指针(接收者需要将数据存放到哪里)
typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.size)
// No need for cgo write barrier checks because dst is always
// Go memory.
memmove(dst, src, t.size) // 将要发送来的数据src直接移动给了要接收的协程的变量dst
}
- 运行完 sendDirect 后,会运行 goready 唤醒等待的协程
func goready(gp *g, traceskip int) {
systemstack(func() {
ready(gp, traceskip, true)
})
}
5.2.2 放入缓存
放入缓存:没有 G 在休眠等待,但是有缓存空间
放入缓存实现步骤:
- 获取可存入的缓存地址
- 直接将数据存放到缓存中
- 维护缓存的索引
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
...
// 判断是否可以从等待队列中获取协程
...
// 判断缓存区是否还可以存放数据(缓存数量 < 最多能缓存的个数)
if c.qcount < c.dataqsiz {
// Space is available in the channel buffer. Enqueue the element to send.
// 1. 从环形缓存中拿出一个可以用的缓存单元
qp := chanbuf(c, c.sendx)
if raceenabled {
racenotify(c, c.sendx, nil)
}
// 2. 将数据存放到该缓存单元中
typedmemmove(c.elemtype, qp, ep)
// 3. 维护环形缓存的索引
c.sendx++
if c.sendx == c.dataqsiz {
c.sendx = 0
}
c.qcount++
unlock(&c.lock)
return true
}
...
}
总结:放入缓存的逻辑不需要与其他任何的协程有关联,只需要判断缓存区是否还有空间并存放到缓存中即可,如果缓存满了,会进入休眠等待
5.2.3 休眠等待
休眠等待:
- 没有G休眠等待,并且缓存区满了或没有缓存区
- 协程自己进入发送队列,休眠等待
休眠等待实现步骤:
- 将自己包装成 sudog
- 将 sudog 存放到 sendq 队列
- 休眠并解锁(在操作channel中的数据的时候,需要加 Mutex 锁,协程操作channel后进入休眠状态,将 Mutex 解锁)
- 注意:当休眠的协程被唤醒时,数据已经被取走了,需要维护其他数据,例如:索引....
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// 缓存区已经满了,执行下面的逻辑
...
// Block on the channel. Some receiver will complete our operation for us.
gp := getg() // 1. 拿到自己的协程结构体
mysg := acquireSudog() // 2. 将自己包装成sudog
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 // 3. 自己要发送的数据,记录在elem中
mysg.waitlink = nil
mysg.g = gp // 4. 自己的协程的指针
mysg.isSelect = false
mysg.c = c
gp.waiting = mysg
gp.param = nil
c.sendq.enqueue(mysg) // 5. 将自己存放到sendq休眠队列中
atomic.Store8(&gp.parkingOnChan, 1)
// 6. 休眠,协程会卡在这句话
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
// 7. 被唤醒后执行下面的逻辑
...
return true
}
Channel 数据发送总结:
- 编译阶段,会将
ch <-操作转换为 runtime.chansend1() 方法 - 直接发送时,将数据直接拷贝给目标变量
- 放入缓存时,将数据存放到环形缓存中,成功返回
- 休眠等待时,将自己包装成 sudog 并存放到 sendq 中,休眠
5.3 Channel 数据接收
Go 语言中 <- ch 表示将数据从 channel 中取出来
-
该操作会在编译的时候将
i := <- ch转换为 runtime.chanrecv1() 方法 -
如果使用两个参数接收:
i, ok := <- ch会将其转换为 runtime.chanrecv2() 方法 -
最终两个方法都会调用 chanrecv 方法
// entry points for <- c from compiled code
//go:nosplit
func chanrecv1(c *hchan, elem unsafe.Pointer) {
chanrecv(c, elem, true)
}
//go:nosplit
func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
_, received = chanrecv(c, elem, true)
return
}
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
// raceenabled: don't need to check ep, as it is always on the stack
// or is new memory allocated by reflect.
if debugChan {
print("chanrecv: chan=", c, "\n")
}
if c == nil {
if !block {
return
}
gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
throw("unreachable")
}
// Fast path: check for failed non-blocking operation without acquiring the lock.
if !block && empty(c) {
if atomic.Load(&c.closed) == 0 {
return
}
if empty(c) {
// The channel is irreversibly closed and empty.
if raceenabled {
raceacquire(c.raceaddr())
}
if ep != nil {
typedmemclr(c.elemtype, ep)
}
return true, false
}
}
var t0 int64
if blockprofilerate > 0 {
t0 = cputicks()
}
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
}
if c.qcount > 0 {
// Receive directly from queue
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
}
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)
// 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
}
数据接收时有四种情况:
- 有等待的 g,从 g 接收
- 有等待的 g,从缓存中接收
- 接收缓存
- 阻塞接收
5.3.1 有等待的 g,从 g 接收
有等待的 g,从 g 接收:
- 接收数据前,已经有 g 在休眠等待发送
- Channel 中没有缓存
- 将数据直接从 g 中拷贝出来,并唤醒 g
有等待的 g,从 g 接收实现步骤:
- 判断是否有 g 在发送队列中等待
- 判断 channel 是否有缓存
- 直接从等待的 g 中取走数据,唤醒 g
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
...
lock(&c.lock)
// 判断channel是否已经关闭了
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 {
// 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方法
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true, true
}
...
return true, success
}
- 查看 recv 方法
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
// 如果没有缓存或者缓存区为空,执行下面的逻辑
if c.dataqsiz == 0 {
if raceenabled {
racesync(c, sg)
}
if ep != nil {
// copy data from sender
// 执行recvDirect方法
recvDirect(c.elemtype, sg, ep)
}
} else {
...
}
// 维护协程状态
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)
}
- 查看 recvDirect() 方法
func recvDirect(t *_type, sg *sudog, dst unsafe.Pointer) {
// dst is on our stack or the heap, src is on another stack.
// The channel is locked, so src will not move during this
// operation.
src := sg.elem
typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.size)
// 直接将数据移动到目标地址即可
memmove(dst, src, t.size)
}
5.3.2 有等待的g,从缓存接收
有等待的g,从缓存接收:
- 接收数据前,已经有 G 在休眠等待发送
- 而且这个 Channel 有缓存
- 从缓存取走一个数据
- 将休眠 G 的数据放进缓存,唤醒 G
有等待的g,从缓存接收实现步骤:
- 判断有 G 在发送队列等待,进入 recv()
- 判断此 Channel 有缓存
- 从缓存中取走一个数据
- 将 G 的数据放入缓存,唤醒 G
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
...
// 1. 判断是否有协程在休眠等待
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).
// 2. 执行 recv 方法
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true, true
}
...
}
- 执行 recv 方法
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
// 1. 判断是否有缓存,有则执行else
if c.dataqsiz == 0 {
...
} else {
// 2. 从缓存中取出一个数据
qp := chanbuf(c, c.recvx)
if raceenabled {
racenotify(c, c.recvx, nil)
racenotify(c, c.recvx, sg)
}
// copy data from queue to receiver
if ep != nil {
// 3. 将取出来的数据直接拷贝给接收g的elem中
typedmemmove(c.elemtype, ep, qp)
}
// copy data from sender to queue
// 4. 将等待的协程的数据拷贝到缓存中
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
}
...
// 唤醒等待的协程
goready(gp, skip+1)
}
5.3.3 接收缓存
接收缓存:没有协程等待,且缓存中有数据
接收缓存实现步骤:
- 判断队列中是否有协程等待
- 判断 Channel 中是否有缓存数据
- 从缓存中取出数据
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
...
// 缓存中有数据
if c.qcount > 0 {
// Receive directly from queue
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.3.4 阻塞接收
阻塞接收:
- 没有 G 在休眠等待,而且没有缓存或缓存空
- 自己进入接收队列,休眠等待
阻塞接收实现步骤:
- 判断没有 G 在发送队列等待
- 判断此 Channel 无缓存
- 将自己包装成 sudog
- sudog 放入接收等待队列,休眠.
- 唤醒时,发送的 G 已经把数据拷贝到位
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
...
// 无法拿到数据,走如下逻辑
// no sender available: block on this channel.
// 1. 拿到自己的g结构体
gp := getg()
mysg := acquireSudog() // 2. 包装为sudog
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
gp.waiting = mysg
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.param = nil
// 3. 将sudog存放到接收等待队列中
c.recvq.enqueue(mysg)
// Signal to anyone trying to shrink our stack that we're about
// to park on a channel. The window between when this G's status
// changes and when we set gp.activeStackChans is not safe for
// stack shrinking.
atomic.Store8(&gp.parkingOnChan, 1)
// 4. 休眠
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)
// 5. 唤醒的时候,不需要考虑数据的问题,数据已经被发送的协程塞给他了,对应发送数据的直接发送
// 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 数据接收总结:
- 编译阶段,<-ch 会转化为 chanrecv()
- 有等待的 G,且无缓存时,从 G 接收
- 有等待的 G,且有缓存时,从缓存接收
- 无等待的 G,且缓存有数据,从缓存接收
- 无等待的 G,且缓存无数据,等待喂数据
5.4 实现非阻塞 channel
5.4.1 select 底层实现
前面我们查看源码的时候,在接收数据的时候,可以传递非阻塞的标志(chanrecv 方法的 block 参数),该参数为 false 表示非阻塞的 channel
当我们使用 select 关键字的时候,传递的参数为 false
func main() {
ch1 := make(chan int, 5)
ch2 := make(chan int)
select {
case <-ch1: // 如果可以从ch1中取数据,执行下面的逻辑
fmt.Println("ch1 recv")
case ch2 <- 1: // 如果可以往ch2中发送数据,执行下面的逻辑
fmt.Println("ch2 recv")
default: // 上面的逻辑都走不通,执行default
fmt.Println("none")
}
}
select 的原理:同时存在接收、发送、默认路径时
- 查看是否存在可以即时执行的 case
- 没有则走 default
- 如果没有 default,则会将 select 注册到 ch1 的接收队列,ch2 的发送队列中,然后 select 休眠
- 当 ch1 和 ch2 中有一个唤醒 select,就会走对应的逻辑
5.4.2 Timer
Timer 提供了一个工具,可以定时的塞入数据,如下:
func main() {
t := time.NewTimer(time.Second)
<-t.C // 会在倒计时结束后往channel中塞入一个数据,然后我们再将该数据取出来,x
fmt.Println("hello")
}

浙公网安备 33010602011771号