Go channel 写流程

42 阅读4分钟

我们看一下底层源码是怎么做的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压力等。