文章目录
- channel是什么
- channel的数据结构
- channel操作
- Select
- 检验
-
- [1、channel 是线程安全的吗?](#1、channel 是线程安全的吗?)
- [2、channel 的底层实现原理](#2、channel 的底层实现原理)
- [3、对channel 进行读,写,关闭操作会怎么样?](#3、对channel 进行读,写,关闭操作会怎么样?)
-
- [1. 对nil的channel进行读、写、关闭](#1. 对nil的channel进行读、写、关闭)
- [2. 对不为nil,并且未关闭的channel操作,读和写都有两种情况](#2. 对不为nil,并且未关闭的channel操作,读和写都有两种情况)
- [3. 对已经关闭的channel进行写、关闭、读](#3. 对已经关闭的channel进行写、关闭、读)
- 4、运行下面代码,会得到什么结果?
- 5、运行下面代码,会得到什么结果?
channel是什么
顾名思义,channel就是一个通信管道,被设计用于实现goroutine之间的通信
Go语言尊崇的设计思想是:以通信的方式来共享内存,而不是通过共享内存来实现通信,channel就是这一思想的体现
channel的数据结构
channel的功能比较复杂,所以就不会是几个字节就能实现的,所以需要一个复杂的struct来承接channel的作用,也就是下文的hchan结构体
要注意channel是直接分配到堆上的 ,因为channel从设计理念上看,就是用于goroutine之间的通信,作用域和生命周期不会被限制在一个函数中
runtime.mutex和sync.Mutex的区别

hchan中的字段
runtime.hchan的类型定义在源码 src/runtime/chan.go中:
go
type hchan struct {
qcount uint // channel 环形数组中元素的数量
dataqsiz uint // channel 环形数组的容量
buf unsafe.Pointer // 指向channel 环形数组的一个指针
elemsize uint16 // 元素所占的字节数
closed uint32 // 是否关闭
timer *timer // timer feeding this chan
elemtype *_type // 元素类型
sendx uint // send index 下一次写的位置
recvx uint // receive index 下一次读的位置
recvq waitq // list of recv waiters 读等待队列
sendq waitq // list of send waiters 写等待队列
bubble *synctestBubble
// 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 // runtime.mutex,保证channel并发安全
}


对于channel,我们可以将数据缓存到其中,所以有一个buf数组,用来缓冲数据,又因为channel可以同时提供读写功能,所以我们有sendx 和 recvx分别指向下一次写和下一次读的位置,buf、sendx、recvx 就构成了一个环形数组,每次读或写超过最后一个下标,就会回到下标0处。
用qcount表示buf中元素数量,用dataqsiz表示buf的容量。
因为channel设计是用在多个goroutine之间的通信上的,所以需要一把mutex来保护读、写、关闭操作的并发安全【在并发/编程语境里,"同步/异步"最核心的区别就是 :发起一个操作后,要不要等它完成。】
因为channel提供一个读和写等待队列(recvq和sendq)来帮助goroutine在未完成读写操作后,可以被阻塞挂起,然后等待channel通信来临时,再被唤醒调度
下面我们看一下recvq 和 sendq的结构,也就是waitq结构体,可以把waitq看作是一个链表构成的队列
waitq结构体
go
type waitq struct {
first *sudog // sudog队列的队头指针
last *sudog // sudog队列的队尾指针
}
sudog结构体
介绍一下sudog这个结构体,sudog可以看作是对阻塞挂起的g的一个封装,用多个sudog来构成等待队列
当 goroutine 在不可立即完成的 channel 操作上需要阻塞(例如:向已满的缓冲 channel 发送、从空的缓冲 channel 接收,或无缓冲 channel 缺少配对方)时,运行时会将该 goroutine 关联成一个 sudog,挂到对应等待队列(sendq 或 recvq)上并 park。随后当另一侧操作到来使其能配对完成时,通常会唤醒队列中的一个 goroutine;而在 close(ch) 时,会唤醒相关等待者(接收者通常是全部)。

下面看一下sudog结构(只留下主要字段):
go
type sudog struct {
g *g // 绑定的goroutine
next *sudog // 前后指针
prev *sudog
elem unsafe.Pointer // 存储元素的容器
isSelect bool // 标识是不是因为select操作封装的sudog
// 为true,表示这个sudog是因为channel 通信唤醒的
// 为false,表示这个sudog是因为channel close唤醒的
success bool
c *hchan // 绑定的channel
}
这里关注下elem字段,elem作为收发数据的容器
当向channel发送数据时,elem代表将要写进channel的元素地址
当从channel读取数据时,elem代表要从channel中读取的元素地址
channel操作
channel初始化
Go语言中,我们只能通过make函数来初始化一个channel ,runtime会调用runtime.makechan函数来完成channel的初始化工作
源码位于src/runtime/chan.go中:
go
func makechan(t *chantype, size int) *hchan {
// channel 元素类型
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:
// channel无缓冲 or 元素大小为0,只需要分配一个hchan
// 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.Pointers():
// channel 元素不包含指针,hchan和buf 一起分配
// 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:
// channel 元素包含指针,hchan和buf 分开分配
// 因为 申请的span 为scan 和 noscan,无法一起分配
// Elements contain pointers.
c = new(hchan)
c.buf = mallocgc(mem, elem, true)
}
// channel 的一些初始化
c.elemsize = uint16(elem.Size_)
c.elemtype = elem
c.dataqsiz = uint(size)
if b := getg().bubble; b != nil {
c.bubble = b
}
lockInit(&c.lock, lockRankHchan)
if debugChan {
print("makechan: chan=", c, "; elemsize=", elem.Size_, "; dataqsiz=", size, "\n")
}
return c
}
makechan函数有两个参数 t *chantype, size int,第一个参数代表要创建的channel的元素类型,而第二个参数代表通道环形缓冲的容量大小
可以看出为channel开辟内存分为三种情况:
- channel无缓冲 or 元素大小为0(存储的元素都是空结构体的情况):只需要分配hchan本身结构体大小的内存
- 有缓冲区buf,但元素不包含指针:hchan和buf 一起分配(hchan 和缓冲区会被一次性连续分配,buf 指向 hchan 之后)
- 有缓冲区buf,且元素包含指针类型:hchan和buf 分开分配 【new(T) 等价于按 T 的类型信息 去分配堆对象。hchan 结构体里确实有指针字段(例如 buf unsafe.Pointer、elemtype *type、等待队列指针等),runtime 一般不会为这种"运行时拼出来的结构体 + 动态长度数组"生成一份新的类型/位图来交给 GC(成本和复杂度都很高),所以选择 分开分配:
hchan 按 hchan 的类型扫描,buf 按 elem 的类型扫描。因此它属于 scan object,GC 需要扫描它的指针字段。为了让 GC 能从 hchan.buf 找到并扫描缓冲区里的指针,hchan 就必须是 scan(至少要扫描到 buf 这个指针)】

channel写入
下面是往channel中写入一个数据的例子
go
ch := make(chan int)
ch <- 1 // 往管道里写入1
底层其实就是调用了 runtime.chansend 函数,源码如下:
go
/*
* 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 {
// c: 对应的hchan指针
// ep: 被发送到channel的变量的地址
// block: 发送操作是否阻塞(代表如果send不能立即完成的话,是否阻塞)
// 判断channel是否为nil
if c == nil {
// 如果是非阻塞类型,直接返回false
if !block {
return false
}
// 是阻塞类型(永久性挂起)
gopark(nil, nil, waitReasonChanSendNilChan, traceBlockForever, 2)
throw("unreachable")
}
if debugChan {
print("chansend: chan=", c, "\n")
}
if raceenabled {
racereadpc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(chansend))
}
if c.bubble != nil && getg().bubble != c.bubble {
fatal("send on synctest channel from outside bubble")
}
// 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 full()).
// 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. However, nothing here
// guarantees forward progress. We rely on the side effects of lock release in
// chanrecv() and closechan() to update this thread's view of c.closed and full().
// 非阻塞类型channel and channel没关闭 and channel满了
if !block && c.closed == 0 && full(c) {
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"))
}
// 尝试从读等待队列中取出一个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).
// 读等待队列有goroutine,将写入数据直接交给对应goroutine
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.
// 通过sendx 找到写入位置的地址
qp := chanbuf(c, c.sendx)
if raceenabled {
racenotify(c, c.sendx, nil)
}
// 将ep中的数据写入到qp中
typedmemmove(c.elemtype, qp, ep)
c.sendx++
// 如果sendx == dataqsiz,因为是缓冲数组,如果将snedx置为0
if c.sendx == c.dataqsiz {
c.sendx = 0
}
// channel 中的元素数量+1
c.qcount++
unlock(&c.lock)
return true
}
// 非阻塞类型的写操作走到这一步,不管有没有写入到channel中,都不需要阻塞,直接return
if !block {
unlock(&c.lock)
return false
}
// Block on the channel. Some receiver will complete our operation for us.
gp := getg()
// 取出一个sudog结构
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
// 绑定goroutine
mysg.g = gp
mysg.isSelect = false
// 绑定channel
mysg.c = c
gp.waiting = mysg
gp.param = nil
// 进入写等待队列
c.sendq.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.
gp.parkingOnChan.Store(true)
reason := waitReasonChanSend
if c.bubble != nil {
reason = waitReasonSynctestChanSend
}
// gopark操作
gopark(chanparkcommit, unsafe.Pointer(&c.lock), reason, traceBlockChanSend, 2)
// 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
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发送数据会出现三种情况
case1:channel中有读等待goroutine
go
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// .......
// 加锁
lock(&c.lock)
// 尝试从读等待队列中取出一个goroutine
if sg := c.recvq.dequeue(); sg != nil {
// 读等待队列有goroutine,将写入数据直接交给对应goroutine
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true
}
// .......
}
// send processes a send operation on an empty channel c.
// The value ep sent by the sender is copied to the receiver sg.
// The receiver is then woken up to go on its merry way.
// Channel c must be empty and locked. send unlocks c with unlockf.
// sg must already be dequeued from c.
// ep must be non-nil and point to the heap or the caller's stack.
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
if c.bubble != nil && getg().bubble != c.bubble {
unlockf()
fatal("send on synctest channel from outside bubble")
}
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.
racenotify(c, c.recvx, nil)
racenotify(c, c.recvx, sg)
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
}
}
// 将ep 复制到 sg对应的elem上
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()
}
// 唤醒goroutine
goready(gp, skip+1)
}
// Sends and receives on unbuffered or empty-buffered channels are the
// only operations where one running goroutine writes to the stack of
// another running goroutine. The GC assumes that stack writes only
// happen when the goroutine is running and are only done by that
// goroutine. Using a write barrier is sufficient to make up for
// violating that assumption, but the write barrier has to work.
// typedmemmove will call bulkBarrierPreWrite, but the target bytes
// are not in the heap, so that will not help. We arrange to call
// memmove and typeBitsBulkBarrier instead.
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.
// 将src复制到dst(复制channel对应的类型值)
memmove(dst, src, t.Size_)
}
// goready should be an internal detail,
// but widely used packages access it using linkname.
// Notable members of the hall of shame include:
// - gvisor.dev/gvisor
// - github.com/sagernet/gvisor
//
// Do not remove or change the type signature.
// See go.dev/issue/67401.
//
//go:linkname goready
func goready(gp *g, traceskip int) {
systemstack(func() {
ready(gp, traceskip, true)
})
}
// Mark gp ready to run.
func ready(gp *g, traceskip int, next bool) {
status := readgstatus(gp)
// Mark runnable.
mp := acquirem() // disable preemption because it can be holding p in a local var
if status&^_Gscan != _Gwaiting {
dumpgstatus(gp)
throw("bad g->status in ready")
}
// status is Gwaiting or Gscanwaiting, make Grunnable and put on runq
trace := traceAcquire()
// 状态修改成 待执行
casgstatus(gp, _Gwaiting, _Grunnable)
if trace.ok() {
trace.GoUnpark(gp, traceskip)
traceRelease(trace)
}
// 放入到gmp模型中,重新得到调度
runqput(mp.p.ptr(), gp, next)
wakep()
releasem(mp)
}

-
先拿锁
-
从recvq(读等待队列)里面弹出队列头部的sudog,进入send流程
-
将要写入的数据拷贝得到这个sudog对应的elem数据容器上
-
释放锁
-
唤醒sudog绑定的goroutine(也就是将这个goroutine重新放入到gmp模型中,等待调度)【他会放入当前P(执行唤醒操作的P)的本地队列。 如果当前P的本地队列已满,或者有其他更优的调度策略(比如有空闲的P),它可能会被放入其他P的本地队列。 如果所有P的本地队列都满了,或者在某些特定情况下(例如,长时间等待后被唤醒的goroutine,或者通过runtime.Gosched()主动让出的goroutine),它才可能会被放入全局运行队列】
case2:channel中没有读等待goroutine,并且环形缓冲数组里面有剩余空间
go
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// .......
// 加锁
lock(&c.lock)
// 如果环形数组还有容量可以写入
if c.qcount < c.dataqsiz {
// 通过sendx 找到写入位置的地址
qp := chanbuf(c, c.sendx)
if raceenabled {
racenotify(c, c.sendx, nil)
}
// 将ep中的数据写入到qp中
typedmemmove(c.elemtype, qp, ep)
c.sendx++
// 如果sendx == dataqsiz,因为是缓冲数组,如果将snedx置为0
if c.sendx == c.dataqsiz {
c.sendx = 0
}
// channel 中的元素数量+1
c.qcount++
unlock(&c.lock)
return true
}
// ...............
}
// chanbuf(c, i) is pointer to the i'th slot in the buffer.
//
// chanbuf should be an internal detail,
// but widely used packages access it using linkname.
// Notable members of the hall of shame include:
// - github.com/fjl/memsize
//
// Do not remove or change the type signature.
// See go.dev/issue/67401.
//
//go:linkname chanbuf
func chanbuf(c *hchan, i uint) unsafe.Pointer {
return add(c.buf, uintptr(i)*uintptr(c.elemsize))
}
-
先拿锁
-
将数据写入到 sendx指向的位置中
-
sendx++, qcount++
-
释放锁
case3:channel中没有读等待goroutine,并且无剩余空间存放数据
go
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// ......
gp := getg()
// 取出一个sudog结构
mysg := acquireSudog()
// 将ep存入到elem中
mysg.elem = ep
// 绑定goroutine
mysg.g = gp
// 绑定channel
mysg.c = c
// 进入写等待队列队尾
c.sendq.enqueue(mysg)
// gopark操作
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
// 处理状态
gp.waiting = nil
gp.activeStackChans = false
closed := !mysg.success
gp.param = nil
mysg.c = nil
// 回收sudog
releaseSudog(mysg)
return true
}
// Puts the current goroutine into a waiting state and calls unlockf on the
// system stack.
//
// If unlockf returns false, the goroutine is resumed.
//
// unlockf must not access this G's stack, as it may be moved between
// the call to gopark and the call to unlockf.
//
// Note that because unlockf is called after putting the G into a waiting
// state, the G may have already been readied by the time unlockf is called
// unless there is external synchronization preventing the G from being
// readied. If unlockf returns false, it must guarantee that the G cannot be
// externally readied.
//
// Reason explains why the goroutine has been parked. It is displayed in stack
// traces and heap dumps. Reasons should be unique and descriptive. Do not
// re-use reasons, add new ones.
//
// gopark should be an internal detail,
// but widely used packages access it using linkname.
// Notable members of the hall of shame include:
// - gvisor.dev/gvisor
// - github.com/sagernet/gvisor
//
// Do not remove or change the type signature.
// See go.dev/issue/67401.
//
//go:linkname gopark
func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceReason traceBlockReason, traceskip int) {
if reason != waitReasonSleep {
checkTimeouts() // timeouts may expire while two goroutines keep the scheduler busy
}
mp := acquirem()
gp := mp.curg
status := readgstatus(gp)
// gopark 只能从running或者scanrunning状态进入
if status != _Grunning && status != _Gscanrunning {
throw("gopark: bad g status")
}
mp.waitlock = lock
mp.waitunlockf = unlockf
gp.waitreason = reason
mp.waitTraceBlockReason = traceReason
mp.waitTraceSkip = traceskip
releasem(mp)
// can't do anything that might move the G between Ms here.
// 去系统栈空间(g0)挂起当前M上的g
mcall(park_m)
}
// park continuation on g0.
func park_m(gp *g) {
mp := getg().m
trace := traceAcquire()
// If g is in a synctest group, we don't want to let the group
// become idle until after the waitunlockf (if any) has confirmed
// that the park is happening.
// We need to record gp.bubble here, since waitunlockf can change it.
bubble := gp.bubble
if bubble != nil {
bubble.incActive()
}
if trace.ok() {
// Trace the event before the transition. It may take a
// stack trace, but we won't own the stack after the
// transition anymore.
trace.GoPark(mp.waitTraceBlockReason, mp.waitTraceSkip)
}
// N.B. Not using casGToWaiting here because the waitreason is
// set by park_m's caller.
casgstatus(gp, _Grunning, _Gwaiting)
if trace.ok() {
traceRelease(trace)
}
// 解绑当前G
dropg()
if fn := mp.waitunlockf; fn != nil {
ok := fn(gp, mp.waitlock)
mp.waitunlockf = nil
mp.waitlock = nil
if !ok {
trace := traceAcquire()
casgstatus(gp, _Gwaiting, _Grunnable)
if bubble != nil {
bubble.decActive()
}
if trace.ok() {
trace.GoUnpark(gp, 2)
traceRelease(trace)
}
execute(gp, true) // Schedule it back, never returns.
}
}
if bubble != nil {
bubble.decActive()
}
// 新一轮调度使命
schedule()
}
-
锁保护步骤同样有的
-
获取一个sudog结构,绑定对应的channel,goroutine,还有ep指针
-
将sudog放入channel的写等待队列(sendq)
-
runtime.gopark(挂起当前goroutine,可以看作是解绑当前g(协程)和m(线程),然后开启下一轮调度)【gopark就是把goroutine的可执行状态改成待执行(等待一些条件就位),触发和恢复也是由runtime来调度的】
特殊case
第一种特殊情况:写入的channel为nil
go
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// c:对应的hchan指针
// ep:被发送到channel的变量的地址
// block:发送操作是否阻塞(代表如果send不能立即完成的话,是否阻塞)
// 判断channel是否为nil
if c == nil {
// 如果是非阻塞类型,直接返回false
if !block {
return false
}
// 是阻塞类型(永久性挂起)
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable")
}
// ............
}
- 当channel为nil的时候,对channel进行写操作,会导致当前goroutine永久性挂起 ,如果当前goroutine是main goroutine的话,还会导致整个程序退出
第二种特殊情况:channel已经关闭,还想进行写操作
go
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// 加锁
lock(&c.lock)
// channel关闭了,执行写操作,触发panic
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
// ............
}
- 当channel已经被关闭,再向channel写数据,会出现panic
channel读取
从channel读取数据的编码形式如下:
go
ch := make(chan, int)
v := <- ch // 直接读取
v, ok <- ch // ok判断读取的是否有效
底层其实就是调用了 runtime.chanrecv 函数,源码如下:
ep 代表一个指针,它指向一个外部的变量地址。在代码中,这个变量用于存储从 Go channel 中读取出来的数据。具体来说,ep 是用来存放 channel 接收到的值的地址。
go
// chanrecv receives on channel c and writes the received data to ep.
// ep may be nil, in which case received data is ignored.
// If block == false and no elements are available, returns (false, false).
// Otherwise, if c is closed, zeros *ep and returns (true, false).
// Otherwise, fills in *ep with an element and returns (true, true).
// A non-nil ep must point to the heap or the caller's stack.
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.
// c: 对应的hchan指针
// ep: 被发送到channel的变量的地址
// block: 发送操作是否阻塞(代表如果send不能立即完成的话,是否阻塞)
// selected和received
// 如果received为true,则说明数据是从channel接收到的
// 如果received为false, selected为true, 说明channel是通道关闭,并且得到零值
// 如果received为false, selected为false, 则是因为非阻塞操作返回
if debugChan {
print("chanrecv: chan=", c, "\n")
}
// 判断channel是否为nil
if c == nil {
// 如果是非阻塞类型,直接返回
if !block {
// 两个false, 通道不想阻塞而返回
return
}
// 是阻塞类型(永久性挂起)
gopark(nil, nil, waitReasonChanReceiveNilChan, traceBlockForever, 2)
throw("unreachable")
}
if c.bubble != nil && getg().bubble != c.bubble {
fatal("receive on synctest channel from outside bubble")
}
if c.timer != nil {
c.timer.maybeRunChan(c)
}
// Fast path: check for failed non-blocking operation without acquiring the lock.
// 非阻塞类型,并且channel是空的(无缓冲,且sendq为空)
if !block && empty(c) {
// After observing that the channel is not ready for receiving, we observe whether the
// channel is closed.
//
// Reordering of these checks could lead to incorrect behavior when racing with a close.
// For example, if the channel was open and not empty, was closed, and then drained,
// reordered reads could incorrectly indicate "open and empty". To prevent reordering,
// we use atomic loads for both checks, and rely on emptying and closing to happen in
// separate critical sections under the same lock. This assumption fails when closing
// an unbuffered channel with a blocked send, but that is an error condition anyway.
if atomic.Load(&c.closed) == 0 {
// 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.
// 通道没有关闭,返回两个false, 通道不想阻塞而返回
return
}
// The channel is irreversibly closed. Re-check whether the channel has any pending data
// to receive, which could have arrived between the empty and closed checks above.
// Sequential consistency is also required here, when racing with such a send.
if empty(c) {
// 将ep清空
// The channel is irreversibly closed and empty.
if raceenabled {
raceacquire(c.raceaddr())
}
if ep != nil {
typedmemclr(c.elemtype, ep)
}
// 通道关闭了,但是channel是空的(无缓冲,且sendq为空),返回true,false,表示channel因为通道为空,收到零值
return true, false
}
}
var t0 int64
if blockprofilerate > 0 {
t0 = cputicks()
}
// 加锁
lock(&c.lock)
// 如果channel已经关闭
if c.closed != 0 {
if c.qcount == 0 {
if raceenabled {
raceacquire(c.raceaddr())
}
unlock(&c.lock)
// 清空ep
if ep != nil {
typedmemclr(c.elemtype, ep)
}
// 返回true, false, 说明channel关闭,并且得到零值
return true, false
}
// The channel has been closed, but the channel's buffer have data.
} else {
// 如果通道没有关闭,并且写队列里面有 等待的goroutine
// Just found waiting sender with not closed.
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
}
}
// channel中有元素
if c.qcount > 0 {
// Receive directly from queue
qp := chanbuf(c, c.recvx)
if raceenabled {
racenotify(c, c.recvx, nil)
}
// 将recvx上的 数据 写入到ep中
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
// 清空recvx上的数据
typedmemclr(c.elemtype, qp)
// index++
c.recvx++
// 环形数组操作
if c.recvx == c.dataqsiz {
c.recvx = 0
}
// 元素数量--
c.qcount--
unlock(&c.lock)
// 返回true, true, 说明channel中有元素,并且channel没有关闭
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
// 包装sudog进入读队列 (recvq)
c.recvq.enqueue(mysg)
if c.timer != nil {
blockTimerChan(c)
}
// 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.
gp.parkingOnChan.Store(true)
reason := waitReasonChanReceive
if c.bubble != nil {
reason = waitReasonSynctestChanReceive
}
// 挂起当前goroutine,等待goready唤醒
gopark(chanparkcommit, unsafe.Pointer(&c.lock), reason, traceBlockChanRecv, 2)
// someone woke us up
if mysg != gp.waiting {
throw("G waiting list is corrupted")
}
if c.timer != nil {
unblockTimerChan(c)
}
// 清空一些状态
gp.waiting = nil
gp.activeStackChans = false
if mysg.releasetime > 0 {
blockevent(mysg.releasetime-t0, 2)
}
success := mysg.success
gp.param = nil
mysg.c = nil
// 回收sudog
releaseSudog(mysg)
return true, success
}


selected 主要表示是否成功选择了一个通道(是否发生了接收操作)。
received 主要表示是否从通道实际接收到数据。
往channel读取数据会出现三种情况
case1:channel中有写等待goroutine
go
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
// c: 对应的hchan指针
// ep: 被发送到channel的变量的地址
// ...
// 加锁
lock(&c.lock)
// 如果通道没有关闭,并且写队列里面有 等待的goroutine
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
}
// .......
}
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
// A. 无缓冲 channel:直接从发送方拷贝到接收方
if c.dataqsiz == 0 {
if ep != nil {
// channel无容量, 将sudog 对应的数据写给ep
recvDirect(c.elemtype, sg, ep)
}
} else {
// B. 有缓冲且队列里有等待发送者:做"队头出队 + 队尾入队"的等价交换
// 到这一步, channel 环形数组一定是满的 (因为sendq里面有等待者)
// recvx对应的位置的地址
// buffer 的队头槽位
qp := chanbuf(c, c.recvx)
// 将qp上的数据(环形缓冲#recvx 指向的数据) 写入ep
// 先把队头给接收方
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
// 并且sudog上的数据写入 qp
// 再把发送方的数据塞回这个槽位
// 接收方取走队头元素(给 ep),然后把等待发送者的元素立刻填进刚释放出来的队头槽位。
typedmemmove(c.elemtype, qp, sg.elem)
// 读index++
c.recvx++
// 环形数组处理
if c.recvx == c.dataqsiz {
c.recvx = 0
}
// 同步sendx 和 recvx
c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
}
// 重置这个sudog状态
sg.elem = nil
// 找到当初阻塞的发送方 goroutine
gp := sg.g
// 释放锁
unlockf()
gp.param = unsafe.Pointer(sg)
// 告诉它"发送成功了"
sg.success = true
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
// 唤醒对应的写等待goroutine
// 把它从等待态唤醒,重新进入可运行队列
goready(gp, skip+1)
}
-
先拿锁
-
从sendq(写等待队列)里面弹出队列头部的sudog,进入recv流程
-
如果channel无缓冲区,直接读取sudog里面的数据,并唤醒sudog对应goroutine
-
如果channel有缓冲区,读取环形缓冲区recvx下标对应的元素,并将sudog中的元素写入到缓冲区,唤醒sudog对应goroutine
-
释放锁


case2:channel中没有写等待goroutine,并且环形缓冲数组里面有剩余元素
go
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
// c: 对应的hchan指针
// ep: 被发送到channel的变量的地址
// ......
// 加锁
lock(&c.lock)
// channel中有元素
if c.qcount > 0 {
// 取recvx对应地址上的元素
qp := chanbuf(c, c.recvx)
// 将这个元素 写入到ep中
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
// 清空recvx上的数据
typedmemclr(c.elemtype, qp)
// index++
c.recvx++
// 环形数组操作
if c.recvx == c.dataqsiz {
c.recvx = 0
}
// 元素数量--
c.qcount--
unlock(&c.lock)
// 返回true, true, 说明channel中有元素, 并且channel没有关闭
return true, true
}
// ......
}


memmove(to, from, n) 做的事情只有一个:把 from 指向的那 n 个字节复制到 to 指向的内存里(按字节拷贝)。所以它本质上就是"拷贝内存内容"。
-
先拿锁
-
读取recvx指向的数据
-
recvx++, qcount--
-
释放锁
case3:channel中没有写等待goroutine,并且环形缓冲数组里面无剩余元素
go
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
// c: 对应的hchan指针
// ep: 被发送到channel的变量的地址
// ......
// 加锁
lock(&c.lock)
// 获取当前goroutine
gp := getg()
// 获取一个sudog
mysg := acquireSudog()
// 绑定接收指针
mysg.elem = ep
gp.waiting = mysg
// 绑定goroutine
mysg.g = gp
// 绑定channel
mysg.c = c
// 包装sudog进入读队列 (recvq)
c.recvq.enqueue(mysg)
// 挂起当前goroutine, 等待goready唤醒
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)
// 清空channel状态
gp.waiting = nil
gp.activeStackChans = false
success := mysg.success
gp.param = nil
mysg.c = nil
// 回收sudog
releaseSudog(mysg)
return true, success
}
-
锁保护步骤同样有的
-
获取一个sudog结构,绑定对应的channel,goroutine,还有ep指针
-
将sudog放入channel的读等待队列(recvq)
-
runtime.gopark(挂起当前goroutine,可以看作是解绑当前g和m,然后开启下一轮调度)
特殊case
第一种特殊情况:读取的channel为nil
go
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
// 判断channel是否为nil
if c == nil {
// 如果是非阻塞类型, 直接返回
if !block {
// 两个false, 通道不想阻塞而返回
return false, false
}
// 是阻塞类型(永久性挂起)
gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
throw("unreachable")
}
// ......
}
当channel为nil的时候,对channel进行读操作,会导致当前goroutine永久性挂起,如果当前goroutine是main goroutine的话,还会导致整个程序退出
第二种特殊情况:channel已经关闭,并且buf里面没有元素
go
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
// ......
// 如果channel已经关闭
if c.closed != 0 {
if c.qcount == 0 {
unlock(&c.lock)
// 清空ep
if ep != nil {
typedmemclr(c.elemtype, ep)
}
// 返回true, false, 说明channel关闭, 并且得到零值
return true, false
}
}
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
}
// ......
}
channel已经关闭,并且没有剩余元素,还想读取channel会得到对应类型的零值
channel关闭
管道的关闭很简单,操作如下
go
ch := make(chan int)
close(ch)
底层其实就是调用了 runtime.closechan 函数,源码如下:
go
func closechan(c *hchan) {
// chan为nil,想要执行关闭操作,直接panic
if c == nil {
panic(plainError("close of nil channel"))
}
if c.bubble != nil && getg().bubble != c.bubble {
fatal("close of synctest channel from outside bubble")
}
lock(&c.lock)
if c.closed != 0 {
unlock(&c.lock)
// 通道已经关闭,再次执行关闭的话,直接panic
panic(plainError("close of closed channel"))
}
if raceenabled {
callerpc := sys.GetCallerPC()
racewritepc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(closechan))
racerelease(c.raceaddr())
}
// 通道置为1,表示关闭
c.closed = 1
// 通过一个glist来记录channel中所有goroutine等待者
var glist gList
// release all readers
// 将所有recvq的等待者加入到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
if raceenabled {
raceacquireg(gp, c.raceaddr())
}
glist.push(gp)
}
// release all writers (they will panic)
// 将所有sendq的等待者加入到glist中
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)
// Ready all Gs now that we've dropped the channel lock.
// 依次唤醒glist中所有等待者
for !glist.empty() {
gp := glist.pop()
gp.schedlink = 0
goready(gp, 3)
}
}
这里我们直接按照源码来分析流程
- 如果对一个nil的channel执行close操作,会发生panic
- 加锁
- 如果重复关闭channel,也会panic
- 关闭channel(c.closed置为1)
- 将sendq和recvq里面所有等待者加入到glist中
- 唤醒glist中所有等待者(唤醒sudog对应的goroutine)
Select
select也被称为多路select,指的是一个goroutine可以服务多个 channel的读或写操作
源码较长,这里只总结下select的原理:
select分为两种,包含非阻塞型select(包含default分支的) 和 阻塞型select(不包含default分支的)
阻塞型例子:
go
package main
func main() {
ch := make(chan int)
select {
case <-ch:
case ch <- 1:
}
}
非阻塞型例子:
go
package main
func main() {
ch := make(chan int)
select {
case <-ch1:
case ch2 <- 1:
default:
}
}
select的核心原理是,按照随机的顺序执行case,直到某个case完成操作 ,如果所有case的都没有完成操作,则看有没有default分支,如果有default分支,则直接走default,防止阻塞
如果没有的话,需要将当前goroutine 加入到所有case对应channel的等待队列中,并挂起当前goroutine,等待唤醒。
如果当前goroutine被某一个case上的channel操作唤醒后,还需要将当前goroutine从所有case对应channel的等待队列中剔除
检验
1、channel 是线程安全的吗?
是的,一般来说,我们对channel就只有读,写,关闭三种操作,这三种操作,channel底层数据结构都用同一把runtime.Mutex来进行保护
2、channel 的底层实现原理
channel的底层结构体叫做runtime.hchan
-
拥有一把runtime.mutex来保证channel进行读,写,关闭操作逻辑的并发安全
-
通过读写索引(sendx,recvx)和一个buf数组 实现一个环形缓冲队列,可以让channel拥有存储数据的能力
-
拥有读写等待队列,当一个goroutine对channel进行读或写操作,操作无法及时完成的时候,可以进入到等待队列等待,当前goroutine也被runtime.gopark进行挂起
-
而读写操作也能取出 等待队列里面的goroutine,通过runtime.goready将等待中goroutine唤醒(放入gmp模型中),等待GMP的调度
3、对channel 进行读,写,关闭操作会怎么样?
1. 对nil的channel进行读、写、关闭
都会造成当前goroutine永久阻塞(如果当前goroutine是main goroutine,则会让整个程序直接报fatal error 退出) ,关闭则会发生panic (但这里我们不要忘记,还有一种叫做非阻塞的方式操作channel ,这种模式下,就算对为nil的channel读写,也不会阻塞的)
go
package main
import (
"time"
)
// 为nil的channel
var ch chan int
func main() {
// 对nil channel进行读操作
// receiveExample1()
// receiveExample2()
// 对nil channel进行写操作
// sendExample1()
// sendExample2()
// 对nil channel进行close操作
// close(ch)
// 非阻塞模式
// select {
// case <-ch:
// fmt.Println("1")
// default:
// fmt.Println("default")
// }
time.Sleep(1 * time.Second)
}
// 在主goroutine对nil channel进行读
func receiveExample1() {
<-ch
}
// 在普通goroutine对nil channel进行读
func receiveExample2() {
go func() {
<-ch
}()
}
// 在主goroutine对nil channel进行写
func sendExample1() {
ch <- 1
}
// 在普通goroutine对nil channel进行写
func sendExample2() {
go func() {
ch <- 1
}()
}
2. 对不为nil,并且未关闭的channel操作,读和写都有两种情况
a. 读操作:
-
i. 成功读取: 如果channel中有数据,直接从channel里面读取,如果此时写等待队列里面有goroutine,还需要将队列头部goroutine数据写入到channel中,并唤醒这个goroutine;如果channel没有数据,就尝试从写等待队列中读取数据,并做对应的唤醒操作【指channel没有数据,但有写等待者,就是无缓冲的channel】
-
ii. 阻塞挂起(读操作无法及时完成): channel里面没有数据 并且 写等待队列为空,则当前goroutine 加入读等待队列中,并挂起,等待唤醒
b. 写操作
-
i. 成功写入: 如果channel 读等待队列不为空,则取 头部goroutine,将数据直接复制给这个头部goroutine,并将其唤醒,流程结束;否则就尝试将数据写入到channel 环形缓冲中
-
ii. 阻塞挂起(写操作无法及时完成): 通道里面buf满了 并且 读等待队列为空,则当前goroutine 加入写等待队列中,并挂起,等待唤醒
3. 对已经关闭的channel进行写、关闭、读
对已经关闭的channel进行写和关闭 都会导致panic,而读取是直到读完channel中剩余数据,还想读的话,就会获得零值
4、运行下面代码,会得到什么结果?
go
package main
import "fmt"
func main() {
case1 := make(chan int)
case2 := make(chan int)
close(case1)
close(case2)
select {
case <-case1:
fmt.Println("case1")
case case2 <- 1:
fmt.Println("case2")
default:
fmt.Println("default")
}
}
A: 进入default分支,打印"default"
B: 进入case1分支,打印"case1"
C: 进入case2分支,打印"case2"
D: 程序panic
E: 程序可能panic,也可能打印"case1"
答案:E
go
root@GoLang:~/proj/goforjob# go run main.go
panic: send on closed channel
goroutine 1 [running]:
main.main()
/root/proj/goforjob/main.go:11 +0xb5
exit status 2
root@GoLang:~/proj/goforjob#
go
root@GoLang:~/proj/goforjob# go run main.go
case1
root@GoLang:~/proj/goforjob#
5、运行下面代码,会得到什么结果?
go
package main
func main() {
c := make(chan int, 1)
done := false
for !done {
select {
case <-c:
println(1)
c = nil
case c <- 1:
println(2)
default:
println(3)
done = true
}
}
}
答案是
bash
root@GoLang:~/proj/goforjob# go run main.go
2
1
3
之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!