我们看一下底层源码是怎么做的look:
// c 指向channel的指针
// eq 要发送数据的地址
// block 表示是否允许阻塞
// cellerpc调用法的程序计数器
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// 判断当前channel是否为nil 是否允许阻塞
if c == nil {
if !block {
return false
}
// 阻塞当前channel goroutine将不会被唤醒
// 这个就是声明了一个nil的channel
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable")
}
// 关于调试的
if debugChan {
print("chansend: chan=", c, "\n")
}
// 数据竞争检查
if raceenabled {
racereadpc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(chansend))
}
// 判断是否阻塞
// channel 是否关闭
// full 判断元素大小以及recvq的waitq(阻塞队列的头部第一个是否为nil)
if !block && c.closed == 0 && full(c) {
return false
}
// 是否开启阻塞性能分析
var t0 int64
if blockprofilerate > 0 {
// 记录cpu时间
t0 = cputicks()
}
// 上锁
lock(&c.lock)
// 判断是否已经关闭
// 向一个已经关闭的channel发送数据直接panic
// 然后解锁
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
// 写时存在阻塞读协程
// 如果有接受队列挂起的接收者(就是读取那阻塞)
// 从阻塞的写协程队列中去一个groutine的封装的sudog去执行
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
}
// 判断当前缓冲区是否还有位子可以写入
// 当前队列的元素个数 < 当前channel的大小(初始化设置的缓冲区大小)
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)
// 自增sendx的值
c.sendx++
// 判断如果sendx == 当前channel的大小 直接sendx == 0 形成一个闭环
// 就是变成一个环形数组
if c.sendx == c.dataqsiz {
c.sendx = 0
}
// 数量+1 表示添加了一条数据 然后释放锁 返回
c.qcount++
unlock(&c.lock)
return true
}
// 这里表示 非阻塞 但是缓冲区满了 直接释放锁返回
// 缓冲区中有数据,缓冲区已经满了
// 在被select多路复用的时 没有挂起的接受者
if !block {
unlock(&c.lock)
return false
}
// 缓冲区满 发生堵塞
// Block on the channel. Some receiver will complete our operation for us.
// 构造sudog
// sudog是Go runtime 中用来表示“等待某个资源”的 goroutine。
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
// 挂起sendq,就是准备挂起自己
// 把当前的goroutine挂到channel的sendq上
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.
// 防止goroutine被缩栈
gp.parkingOnChan.Store(true)
// 阻塞当前goroutine,直到被唤醒
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 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.
// 保证当前阻塞的goroutine ep不会被GC扫描到然后回收
KeepAlive(ep)
// 等待唤醒
// someone woke us up.
// 这里唤醒其对应的元素必然被读取协程取走
if mysg != gp.waiting {
throw("G waiting list is corrupted")
}
gp.waiting = nil
gp.activeStackChans = false
// success 字段是sudog中判断是否通信成功
// 这里就是判断通道是否关关闭
closed := !mysg.success
gp.param = nil
if mysg.releasetime > 0 {
blockevent(mysg.releasetime-t0, 2)
}
mysg.c = nil
// 释放sudog
releaseSudog(mysg)
// 如果通道已经关闭报panic
if closed {
if c.closed == 0 {
throw("chansend: spurious wakeup")
}
panic(plainError("send on closed channel"))
}
return true
}
总结流程:
如果往一个nil channel中写入数据将会直接阻塞无法被唤醒。如果向一个已经关闭的channel写入数据直接panic。判断非阻塞写入,channel为关闭,并且缓冲区已经满了的情况下,直接返回失败。这个时候上锁,有一个为阻塞写入,从sendq中去取goroutine的封装对象sudog,判断如果接受队列别挂起,直接发送数据不进入缓冲区,然后解锁返回。
接着判断缓冲是否已经满(通过判断当前channel的qcount和dataqsiz去判断):
还有空间可以写入:确定发送法的下标sendx然后进行++就的到需要写入的在那个位置中,如果sendx == dataqsiz 如果想等则sendx == 0,这样就完成了一个闭环。
还有空间(buf)已经满了:
这个时候就开始阻塞了,然后进行构造sudog(把goroutine封装到sendq队列中)将自己挂起到当前channel等待被唤醒。被唤醒后判断当前channel是否可用,执行释放sudog,如果channel关闭直接panic。
这里提一下,为什么使用环形数组:节省内存,减少GC压力等。