go语言channel
go语言channel
设计原理
go语言中提倡:不要通过共享内存方式进行通信,而应该通过通信的方式共享内存。
在很多编程语言中,多个线程传递数据的方式一般是共享内存,为了解决线程竞争,我们需要限制同一时间能够读写这些变量的线程数量,然而这与go语言的设计并不相同。
虽然在go语言中也能使用共享内存加互斥锁进行通信,但是go语言提供了一种不同的并发模型——通信顺序进程(communicating sequential processes,CSP)。goroutine和channel分别对应CSP中的实体和传递信息的媒介
channel在运行时的内部表示是runtime.hchan,该结构体中包含了用于保护成员变量的互斥锁,从某种程度上说,channel是一个用于同步和通信的有锁队列。
数据结构
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
}
sendq和recvq存储了当前channel由于缓冲区空间不足而阻塞的goroutine列表,这些等待队列使用双向链表runtime.waitq表示,链表中所有元素都是runtime.sudog结构
type waitq struct {
first *sudog
last *sudog
}
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
// isSelect indicates g is participating in a select, so
// g.selectDone must be CAS'd to win the wake-up race.
isSelect bool
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
}
runtime.sudog表示一个在等待列表中的goroutine,该结构中存储了两个分别指向前后runtime.sudog的指针以构成链表。
创建channel
func makechan(t *chantype, size int) *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")
}
mem, overflow := math.MulUintptr(elem.size, uintptr(size))
if overflow || mem > maxAlloc-hchanSize || size < 0 {
panic(plainError("makechan: size out of range"))
}
// Hchan does not contain pointers interesting for GC when elements stored in buf do not contain pointers.
// 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.
var c *hchan
switch {
case mem == 0:
// Queue or element size is zero.
c = (*hchan)(mallocgc(hchanSize, nil, true))
// Race detector uses this location for synchronization.
c.buf = c.raceaddr()
case elem.ptrdata == 0:
// Elements do not contain pointers.
// Allocate hchan and buf in one call.
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
// Elements contain pointers.
c = new(hchan)
c.buf = mallocgc(mem, elem, true)
}
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中收发元素的类型和缓冲区大小初始化runtime.hchan和缓冲区:
- 如果当前channel中不存在缓冲区,那么只会为runtime.hchan分配一块内存空间
- 如果当前channel中存储的不是指针类型,会为当前channel和底层数组分配一块连续的内存空间
- 默认情况下会单独为runtime.hchan和缓冲区分配内存
发送数据
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))
}
// 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)
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 {
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
}
// 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)
goparkunlock(&c.lock, waitReasonChanSend, traceEvGoBlockSend, 3)
// Ensure the value being sent is kept alive until the
// receiver copies it out. The sudog has a pointer to the
// stack object, but sudogs aren't considered as roots of the
// stack tracer.
KeepAlive(ep)
// 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没有被关闭并且已经有处于读等待的goroutine,那么runtime.chansend会从接收队列recvq中取出最先陷入等待的goroutine并直接向它发送数据:
发送数据时会调用runtime.send,该函数的执行可以分为两个部分:
- 调用runtime.sendDirect将发送的数据直接复制到
x = <- c
表达式中变量x所在的内存地址上; - 调用runtime.goready将等待接收数据的goroutine标记成可运行状态Grunnable,并把该goroutine放到发送方所在处理器的runnext上等待执行,该处理器在下一次调度时会立刻唤醒数据的接收方。
发送数据只是将接收方的goroutine放到了处理器的runnext中,程序并没有立刻执行该goroutine
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
if raceenabled {
if c.dataqsiz == 0 {
racesync(c, sg)
} else {
// Pretend we go through the buffer, even though
// we copy directly. Note that we need to increment
// the head/tail locations only when raceenabled.
qp := chanbuf(c, c.recvx)
raceacquire(qp)
racerelease(qp)
raceacquireg(sg.g, qp)
racereleaseg(sg.g, qp)
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
}
}
if sg.elem != nil {
sendDirect(c.elemtype, sg, ep)
sg.elem = nil
}
gp := sg.g
unlockf()
gp.param = unsafe.Pointer(sg)
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
goready(gp, skip+1)
}
缓冲区
如果创建的channel包含缓冲区比亲切channel中的数据没有装满
首先使用runtime.chanbuf
计算出下一个可以村塾数据的位置,然后通过runtime.typedmemmove将发送的数据复制到缓冲区中,并增加sendx索引和qcount计数器
如果当前channel的缓冲区未满,向channel发送数据会存储在channel的sendx索引所在位置并将sendx索引加一。因为这里的buf是一个循环数组,所以当sendx等于dataqsiz时会重新回到数组开始的位置
阻塞发送
当channel没有接受者能够处理数据时,向channel发送数据会被下游阻塞。使用select关键字可以向channel非阻塞地发送消息。发送流程:
- 调用
runtime.getg
获取发送数据使用的goroutine - 执行
runtime.acquireSudog
获取runtime.sudog
结构,并设置此次阻塞发送的相关信息,例如发送的channel、是否在select中和待发送数据的内存地址等 - 将刚刚创建并初始化的
runtime.sudog
加入发送等待队列,并设置到当前goroutine的waiting上,表示goroutine正在等待该sudog准备就绪 - 调用
runtime.goparkunlock
令当前goroutine陷入沉睡并等待唤醒 - 被调度器唤醒后会执行一些收尾工作,将一些属性置为0并释放runtime.sudog结构体
- 函数最后会返回true表示这次已经成功向channel发送了数据
小结
简单梳理和总结一下使用ch <- i
表达式向channel发送数据时遇到的几种情况
- 如果当前channel的recvq上存在已经被阻塞的goroutine,那么会直接将数据发送给当前goroutine并将其设置成下一个运行的goroutine
- 如果channel存在缓冲区并且其中还有空闲容量,我们会直接将数据存储到缓冲区sendx所在位置上
- 如果不满足上面两种情况,会创建一个runtime.sudog结构,并将其加入channel的sendq队列中,当前goroutine也会陷入阻塞等待其他协程从channel接收数据
发送数据的过程中包含几个会触发goroutine调度的时机
- 发送数据时发现channel上存在等待接收数据的goroutine,立刻设置处理器的runnext属性,但时并不会立刻触发调度
- 发送数据时并没有找到接收方并且缓冲区已满,这时会将自己加入channel的sendq队列,并调用
runtime.goparkunlock
触发goroutine的调度让出处理器使用权。
接收数据
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.
//
// After observing that the channel is not ready for receiving, we observe that the
// channel is not closed. Each of these observations is a single word-sized read
// (first c.sendq.first or c.qcount, and second c.closed).
// Because a channel cannot be reopened, the later observation of the channel
// being not closed implies that it was also not closed at the moment of the
// first observation. We behave as if we observed the channel at that moment
// and report that the receive cannot proceed.
//
// The order of operations is important here: reversing the operations can lead to
// incorrect behavior when racing with a close.
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)
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(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 {
raceacquire(qp)
racerelease(qp)
}
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
}
// 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
c.recvq.enqueue(mysg)
goparkunlock(&c.lock, waitReasonChanReceive, traceEvGoBlockRecv, 3)
// someone woke us up
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的sendq队列中包含处于等待状态的goroutine时,该函数会取出队头等待的goroutine,处理的逻辑和发送时相差无几,只是发送数据时调用的是runtime.send函数,而接收数据时使用的是runtime.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(c.elemtype, sg, ep)
}
} else {
// Queue is full. Take the item at the
// head of the queue. Make the sender enqueue
// its item at the tail of the queue. Since the
// queue is full, those are both the same slot.
qp := chanbuf(c, c.recvx)
if raceenabled {
raceacquire(qp)
racerelease(qp)
raceacquireg(sg.g, qp)
racereleaseg(sg.g, qp)
}
// copy data from queue to receiver
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
// copy data from sender to queue
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()
}
goready(gp, skip+1)
}
- 如果channel不存在缓冲区:
- 调用runtime.recvDirect将channel发送队列中goroutine存储的elem数据复制到目标内存地址中。
- 如果channel存在缓冲区:
- 将队列中的数据复制到接收方的内存地址中
- 将发送队列头的数据复制到缓冲区中,释放一个阻塞的发送方
无论发生那种情况,运行时都会调用runtime.goready将当前处理器的runnext设置成发送数据的goroutine,在调度器下一次调度时将阻塞的发送方唤醒
缓冲区
当channel的缓冲区中已经包含数据时,从channel中接收数据会直接从缓冲区中recvx的索引位置取出数据进行处理
如果接收数据的内存地址不为空,那么会使用runtime.typedmemmove将缓冲区中的数据复制到内存中、清除队列中的数据并完成收尾工作
收尾工作包括递增recvx,一旦发现索引超过channel的容量时,会将它归零重置循环队列的索引。除此之外,该函数还会减少qcount计数器并释放持有channel的锁
阻塞接收
当channel的发送队列中不存在等待的goroutine并且缓冲区中不存在任何数据时,从channel中接收数据的操作会变成阻塞的,然而不是所有接收操作都是阻塞的,与select语句结合使用时就可能会用到非阻塞的接收操作
在正常接收场景下,我们会使用runtime.sudo将当前goroutine封装成处于等待状态并将其加入接收队列中
完成入队之后,上述代码还会调用runtime.goparkunlock立刻触发goroutine的调度,让出处理器的使用权并等待调度器调度
小结
我们梳理一下从channel中接收数据时可能发生的5中情况:
- 如果channel为空,那么会直接调用runtime.gopark挂起当前goroutine
- 如果channel已经关闭并且缓冲区没有任何数据,runtime.chanrecv会直接返回
- 如果channel的sendq队列中存在挂起的goroutine,会将recvx索引所在的数据复制到接收变量所在的内存空间中并将sendq队列中goroutine的数据复制到缓冲区
- 如果channel的缓冲区中包含数据,那么直接读取recvx索引对应的数据
- 默认情况下回挂起当前goroutine,将runtime.sudog结构加入recvq队列并陷入休眠等待调度器唤醒
我们总结一下从channel接收数据时,会触发goroutine调度的两个时机:
- 当channel为空时
- 当缓冲区中不存在数据并且不存在数据的发送者时
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!