channel的源代码: 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 mutex
}

type waitq struct {
    first *sudog
    last  *sudog
}

sudog是等待的goroutine的表示, 参考 runtime2.go

type sudog struct {
    g *g

    isSelect bool
    next     *sudog
    prev     *sudog
    elem     unsafe.Pointer // data element (may point to stack)

    acquiretime int64
    releasetime int64
    ticket      uint32
    parent      *sudog // semaRoot binary tree
    waitlink    *sudog // g.waiting list or semaRoot
    waittail    *sudog // semaRoot
    c           *hchan // channel
}

通过数据结构, 可以发现, hchan 使用环形队列组织数据, 发送和接受都是用 双向队列维护.

创建

channel的创建, 会调用 #makechan

  1. 如果是无界channel, 只分配基本的内存空间, 用首地址实现同步
  2. 如果是基本类型的channel, 直接分配完整的内存空间, 数据buf指向 内存空间首地址+hchanSize
  3. 指针类型的channel, 直接new方法分配

发送

当我们执行 c <- x 的语句的时候, 发生了什么?

chansend1(汇编入口 ) -> chansend -> sendDirect + goready(唤醒)
  1. 当channel是nil, 并且是阻塞调用, 挂起当前goroutine
  2. 当channel是nil, 并且是非阻塞调用, 直接返回
  3. 发送的时候会进行锁操作, 给已经关闭的channel 发送数据会抛出异常(不符合认知)
  4. 如果有 recvq有等待的goroutine, 直接发送给它, 并进行唤醒
  5. databuf还没有满的情况下, 将数据放入环形缓冲区,
  6. 放不下的情况, 放入senq的等待队列,

接收

当我们执行 x <- c, 执行流程

chanrecv1(汇编入口) -> chanrecv -> recv -> recvDirect + goready(唤醒)

快速检测: 针对channel没有关闭, 并且是非阻塞调用的情况下.

  1. unbuffer channel没有发送的队列(sendq)
  2. buffer channel没有数据发送(qcount=0) 流程:
  3. 当channel是nil, 并且是阻塞调用, 会挂起
  4. 当channel是nil, 并且是非阻塞调用, 直接返回
  5. senq有阻塞的sender, 直接接收, 同时唤醒sender goroutine
  6. 有数据的情况下, 复制数据,
  7. 非阻塞调用, 直接返回
  8. 构建sudog, 放入recvq.

close

方法入口 #closechan

  1. 关闭多次, 会panic
  2. 遍历recvq中阻塞的等待, 进行释放
  3. 遍历sendq中阻塞的等待, 进行释放
  4. 遍历之前需要释放的goroutine, 进行唤醒

select 模型:

select的send:

selectnbsend(编译入口) -> chansend, 不阻塞的调用 
selectnbrecv(编译入口) -> chanrecv, 不阻塞的调用

ok类型的receive:

selectnbrecv2(编译入口) -> chanrecv, 不阻塞的调用

相比之前的receive操作, 多了对nil的判断, 意义不是特别大.

faq: 那么, 在使用上, 向一个nil的 channel 进行 receive 和 send会怎么样? 原理分析: 1. receive 情况: 1. select模型下, 都是使用 非阻塞的调用, 会返回失败 2. 常规使用, 没有ok的情况下, 都是阻塞调用, block forever(gopark), 可能没有线程执行, 会抛出异常 3. 常规使用, 有ok的情况下, 不阻塞, ok是false 2. send 情况: 1. select模型下, 使用非阻塞调用, 会返回失败 2. 常规使用, 都是阻塞调用, block forever (gopark), 可能没有线程执行, 会抛出异常 因为不是golang风格的异常, 所以, 使用defer也不会被检查出来

实验:

  1. 常规空指针接收: receive nil

  2. 常规空指针发送: send nil

  3. 空指针 + select send select send nil

  4. 空指针 + select receive select receive nil

补充说明:

上面的4个实验, 看上去都有报错, 其实是因为 当前的main函数的goroutine被阻塞, 导致没有可执行的goroutine导致的报错, 添加一个可运行的goroutine就可以了, 实验如下图:

just-sleep

参考:

  1. Joshi的博客
  2. nino的博客