Go源码解析——Channel篇

2,506 阅读12分钟

前言

作为Go语言核心的数据结构之一,channel 是支撑Go高并发编程的关键组件。不管是业务开发还是个人学习,对channel的底层原理有所了解都是必要的。

channel的底层其实并不复杂,没有用到高深的结构或设计,如下图所示,主要是围绕着一个环形队列和两个双向链表展开。

image

相信你看完本篇文章,一定能够对channel的底层原理有更深的理解和体会。本文会从channel的底层数据结构初始化发送数据到channel从channel中接收数据关闭channel、通过select操作channel等常见操作展开,并会在文章末尾提出一些社区对于channel的思考和设计,例如:

  • 如何实现无限缓存的channel?

  • 如何实现Lock-Free的channel?

除此之外,本专栏还会更新对golang其他常见数据结构的源码解析,例如slicemap等 ,感兴趣的同学可以持续关注。

专栏地址:[Golang源码解析]

如果有感兴趣的源码和问题可以随时后台交流。

chan数据结构

首先我们对channel的底层数据结构进行介绍,channel的底层数据结构是hchan struct,结构如下:

Go version:go 1.17

代码位置:src/runtime/chan.go

type hchan struct {
    qcount   uint           // 队列中现存元素数量
    dataqsiz uint           // 环形队列容量
    buf      unsafe.Pointer // 环形队列头指针
    elemsize uint16         // 元素大小
    closed   uint32         // channel是否关闭
    elemtype *_type         // 元素类型
    sendx    uint           // 队列已发送位置索引
    recvx    uint           // 队列已接收位置索引
    recvq    waitq          // 由 recv 行为(也就是 <-ch)阻塞在 channel 上的 goroutine 队列
    sendq    waitq          // 由 send 行为 (也就是 ch<-) 阻塞在 channel 上的 goroutine 队列

    lock mutex             //读写锁
}

type waitq struct {
    first *sudog
    last  *sudog
}

划重点:

  • recvq 是读操作阻塞在 channel 的 goroutine 列表
  • sendq 是写操作阻塞在 channel 的 goroutine 列表
  • recvq和sendq都是双向链表 ,FIFO
  • buf使用ring buffer(环形缓存区),优点包括:
    • 适合FIFO式的固定长度队列
    • 可以预先分配固定大小 的数组
    • 允许高效的内存访问 模式
    • 所有的缓存区操作都是O(1),包括消费元素
    • 本质上就是一个带有头尾指针的固定长度数组
  • sudog 是等待goroutine以及数据的封装,是核心数据结构之一

创建channel

首先我们对channel的创建原理进行分析,常见的创建channel方式主要有如下两种:

ch1 := make(chan int)

ch2 := make(chan int,2)

我们可以通过go tool compile -N -l -S main.go命令将代码翻译为汇编指令,或者使用在线工具COMPILTER EXPLORER,以上代码的编译结果如下:go.godbolt.org/z/sM66YdWxs

查看部分带有CALL 指令的内容如下:

CALL    runtime.makechan(SB)
CALL    runtime.makechan(SB)

由以上编译结果,channel的创建函数对应的底层方法为runtime.makechan,makechan()方法主要分为两个部分:合法性验证分配地址空间,接下来我们分别对其进行详细介绍:

代码位置:src**/runtime/chan.go**

2.1 合法性验证

合法性验证部分包括以下步骤:

  • 数据类型大小验证,大于1<<16时异常
  • 内存对齐(降低寻址次数),大于最大内存(8字节数)时异常
  • 传入的size大于堆可分配的最大内存时异常

对应代码如下:

func makechan(t *chantype, size int) *hchan {
    ......
    // 数据类型大小,大于1<<16时异常
    if elem.size >= 1<<16 {​
          throw("makechan: invalid channel element type")​
    }​
    
    // 内存对齐(降低寻址次数),大于最大内存(8字节数)时异常
    if hchanSize%maxAlign != 0 || elem.align > maxAlign {​
          throw("makechan: bad alignment")​
    }​
    ​
    // 传入的size,大于堆可分配的最大内存时异常
    mem, overflow := math.MulUintptr(elem.size, uintptr(size))​
    if overflow || mem > maxAlloc-hchanSize || size < 0 {​
          panic(plainError("makechan: size out of range"))​
    }
    ......
}

2.2 分配地址空间

分配地址空间包括以下步骤:

  • 根据 channel 中收发元素的类型和缓冲区的大小初始化 runtime.hchan 和缓冲区,分为三种情况:
    • 如果不存在缓冲区,分配 hchan 结构体空间,即无缓存 channel
    • 如果 channel 存储的类型不是指针类型,分配连续地址空间,包括 hchan 结构体 + 数据
    • 默认情况包括指针,为 hchan 和 buf 单独分配数据地址空间
  • 更新 hchan 结构体的数据,包括 elemsize、elemtype 和 dataqsiz

对应代码如下:

func makechan(t *chantype, size int) *hchan {​
    ......
    var c *hchan
    switch {
    case mem == 0:
       // buf大小为0,因此只需要为hchan结构体分配空间
       // hchanSize表示空hchan需要占用的字节
       c = (*hchan)(mallocgc(hchanSize, nil, true))
       // raceaddr内部实现为:return unsafe.Pointer(&c.buf)
       c.buf = c.raceaddr()
    case elem.ptrdata == 0:
       // 队列中不存在指针,分配连续地址空间,大小为hchanSize+mem
       c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
       // buf指针指向空hchan占用空间的末尾
       c.buf = add(unsafe.Pointer(c), hchanSize)
    default:
       // 队列包含指针类型
       // 为buf单独开辟mem大小的空间,用来保存所有的数据
       c = new(hchan)
       c.buf = mallocgc(mem, elem, true)
    }
    
    // 更新 hchan 结构体的数据
    c.elemsize = uint16(elem.size)
    c.elemtype = elem
    c.dataqsiz = uint(size)
    lockInit(&c.lock, lockRankHchan)
    ......
}

发送数据到channel

常见的向channel中发送数据的操作如下:

ch <- 1

以上代码的编译结果如下:go.godbolt.org/z/vqYM8Pzdc

查看部分带有CALL 指令的内容如下:

CALL    runtime.chansend1(SB)

由以上编译结果,发送数据到channel对应的底层方法是runtime.chansend1 ,由以下代码,chansend1只是调用了 runtime.chansend,调用时将 block 参数设置成 true,表示当前发送操作是阻塞的:

func chansend1(c *hchan, elem unsafe.Pointer) {
   chansend(c, elem, true, getcallerpc())//阻塞
}

接下来我们对chansend函数进行详细介绍:

代码位置:src/runtime/chan.go

chansend函数主要可以归纳为四部分:

  • 异常检查: 检查channel是否符合接收send请求的状态
  • 同步发送:当存在等待的接收者时,也就是在 recvq 可以获得 waitq,通过 send 方法直接将数据发送给等待的接收者
  • 异步发送:如果没有等待的goroutine,且缓冲区存在空余空间时,将发送的数据写入 channel 的缓冲区
  • 阻塞发送:如果没有等待接收的goroutine且环形队列中也没有数据,则阻塞该goroutine等待其他goroutine从 channel 接收数据,将goroutine和数据打包成sudog存入sendq

4.1 异常检查

chansend()首先对channel进行异常检查:

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
    if c == nil {//判断channal是否为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))
    }
    if !block && c.closed == 0 && full(c) {
    //full为ture的两种情况1)无缓存通道,recvq为空 2)缓存通道,但是buffer已满
      return false
    }
    ......
}

异常检查主要包括:

  • 首先判断channel是否为nil,向一个nil的channel发送数据会发生阻塞。如果是非阻塞模式直接返回false,否则调用gopark,引发以 waitReasonChanSendNilChan 为原因的休眠,并抛出 unreachable 的 fatal error。
  • 当channel不为nil,此时检查channel是否做好接收发送操作的准备,即!block && c.closed == 0 && full(c),
  • full(c)的失败场景包括
    • 无缓冲区且recvq为空
    • 有缓冲区且buf已满
func full(c *hchan) bool {
    if c.dataqsiz == 0 {
        // 无缓冲区channel并且recvq为空
        return c.recvq.first == nil
    }
    // buf已满
    return c.qcount == c.dataqsiz
}

异常检查之后,就是发送的核心逻辑

4.2 同步发送

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
    ...... 
    lock(&c.lock)
    
    if c.closed != 0 {//再次检查channel是否关闭,向已关闭的chan发送元素会引起panic
       unlock(&c.lock)
       panic(plainError("send on closed channel"))
    }
    if sg := c.recvq.dequeue(); sg != nil {//取出第一个非空并且未被选择过的的sudog​
      // 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 是否关闭,如果关闭则抛出 panic。

如果有等待的接受者,也就是recvq队列中有waitq,通过dequeue() 取出头部第一个非空的 sudog,调用 send() 函数直接将数据拷贝到给等待接受的waitq:

func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
   ......
   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()
   }
   goready(gp, skip+1)
}

send()函数主要包含两个工作:

  • 调用 sendDirect() 将数据拷贝到接收变量的内存地址上
  • 调用 goready() 将等待接收的阻塞 goroutine 的状态从 Gwaiting 或者 Gscanwaiting 改变成 Grunnable。下一轮调度时会唤醒这个接收的 goroutine。

4.3 异步发送

如果创建的channel为带缓冲区channel,接收者队列为空时,此时判断缓冲区是否已满,如果缓冲区未满,则进入异步发送逻辑,即将待接收的数据放入缓冲区中,代码如下:

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {​
    ......
    if c.qcount < c.dataqsiz {// 缓冲区未满
          qp := chanbuf(c, c.sendx)//获取缓存区index地址
          if raceenabled {
             racenotify(c, c.sendx, nil)
          }
          typedmemmove(c.elemtype, qp, ep)//数据写入buffer
          c.sendx++
          if c.sendx == c.dataqsiz {
             c.sendx = 0
          }
          c.qcount++
          unlock(&c.lock)
          return true
       }
   ......
}

4.4 阻塞发送

如果没有等待的接受者,且缓存区已满或无缓存channel,则进入阻塞发送 逻辑:

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {​​
    ......  
    gp := getg()​​
    mysg := acquireSudog()​​
    mysg.releasetime = 0​​
    if t0 != 0 {​​
      mysg.releasetime = -1​​
    }​​
    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)​​
    // 把 goroutine 相关的线索结构入队,等待条件满足的唤醒;​
    atomic.Store8(&gp.parkingOnChan, 1)​​
    // goroutine 切走,让出 cpu 执行权限;​
    gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)​​
   
    KeepAlive(ep)​
    ......
}

主要流程就是将goroutine休眠与数据等封装为sudog入sendq队列,细节如下:

  • getg 获取发送数据的 goroutine,用于绑定给一个sudog
  • acquireSudog 获取 sudog 结构,设置好 sudog 要发送的数据和状态。比如发送的 channel、是否在 select 中和待发送数据的内存地址等等。
  • 调用 c.sendq.enqueue 方法将配置好的 sudog 加入待发送的等待队列,并设置到当前 goroutine 的 waiting上,表示 goroutine 正在等待该 sudog 准备就绪
  • gopark 将当前的 goroutine 休眠等待唤醒
  • KeepAlive() 确保发送的值保持活动状态,直到接收者将其复制出来。

在goroutine被调度器唤醒后会将一些属性置零并且释放 sudog 结构体,完成阻塞发送。代码如下:

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {​​​
    ......
    // 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中读取数据

Go 语言中可以使用两种不同的方式去接收 channel 中的数据:

i <- ch
i, ok <- ch

以上代码的编译结果如下:go.godbolt.org/z/4j3q9sWz6

查看部分带有CALL 指令的内容如下:

 CALL    runtime.chanrecv1(SB)
 CALL    runtime.chanrecv2(SB)

虽然不同的接收方式会被转换成 runtime.chanrecv1 和** runtime.chanrecv2 两种不同函数的调用,但是这两个函数最终还是会调用 runtime.chanrecv,此处也是阻塞**调用。

func chanrecv1(c *hchan, elem unsafe.Pointer) {
   chanrecv(c, elem, true)
}

func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
   _, received = chanrecv(c, elem, true)
   return
}

接下来我们对chanrecv函数进行详细介绍:

代码位置:src/runtime/chan.go

chanrecv函数与chansend函数类似,同样可以归纳为四部分:

  • 异常检查: 检查channel是否符合接收recv操作的状态
  • 同步接收: 当存在等待的发送者时,也就是在 sendq 可以获得 waitq,此时对应两种情况:

    • 无缓冲区channel,此时直接从发送方接收数据
    • 有缓冲区channel,此时将缓冲区(buf)头部数据拷贝到接收者内存空间,并将sendq头部goroutine中数据拷贝到buf中
  • 异步接收: 如果没有等待的goroutine,且buf中存在数据时,直接从buf中接收数据
  • 阻塞接收: 如果没有等待发送的goroutine且buf中也没有数据,则阻塞该goroutine等待其他 goroutine向channel 发送数据,将goroutine和数据打包成sudog存入recvq

5.1 异常检查

chanrecv()首先对channel进行异常检查

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
   if debugChan {
      print("chanrecv: chan=", c, "\n")
   }

   if c == nil {
      if !block {
         return
      }
      gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
      throw("unreachable")
   }
   
   if !block && empty(c) {
      if atomic.Load(&c.closed) == 0 {
         return
      }
      if empty(c) {
      // channel不可逆的关闭并且为空
         if raceenabled {
            raceacquire(c.raceaddr())
         }
         if ep != nil {
            typedmemclr(c.elemtype, ep)
         }
         return true, false
      }
   }
   ......
}

异常检查主要包括:

  • 首先判断channel是否为nil,如果是非阻塞模式直接返回,否则调用gopark挂起goroutine。
  • 接下来对channel 进行快速失败检查,检测 channel 是否已经准备好接收recv请求。
  • empty()函数主要在两种情况下为true:
    • 无缓冲区且sendq内没有等待发送的goroutine
    • 有缓冲区且buf为空

empty()代码如下:

func empty(c *hchan) bool {
   if c.dataqsiz == 0 {
      return atomic.Loadp(unsafe.Pointer(&c.sendq.first)) == nil
   }
   return atomic.Loaduint(&c.qcount) == 0
}

异常检查之后,就是接收的核心逻辑

5.2 同步接收

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    ......
    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 {
       recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
       return true, true
    }
    ......
}

接收之前先加锁,保证线程安全。再次对channel的状态(关闭且为空)进行验证,如不符合接收recv请求的状态,直接返回。

如果sendq中有等待的发送者 ,此时调用**recv()**函数完成同步接收。

func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
    //如果没有缓存区,那么直接拷贝发送队列的值
    if c.dataqsiz == 0 {
        if ep != nil {
            // 直接从等待的发送者接收数据
            recvDirect(c.elemtype, sg, ep)
        }
    } else {
            //如果有缓存区,说明缓存区满了并且产生了等待发送的队列
            //从缓存区中获取数据,并且将发送队列的头节点保存的数据写入缓存区中
            qp := chanbuf(c, c.recvx)
            
            //将缓存区的数据拷贝到接收者目标地址
            if ep != nil {
                    typedmemmove(c.elemtype, ep, qp)
            }
            //将发送队列的头节点数据拷贝到当前缓存区位置
            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()
    }
    //将阻塞的发送方头节点goroutine唤醒
    goready(gp, skip+1)
}

recv()方法主要包含以下步骤:

  • 判断channel的缓存区大小
    • 如果缓存区为0,此时直接从sendq中取出队头goroutine,直接从发送方接收值。
    • 如果缓存区不为0,说明此时缓冲区已满,此时发生两次拷贝
      • 首先将缓存区的数据拷贝到接收者目标地址
      • 之后将发送队列sendq头结点的数据拷贝到缓存区位置
  • 调用 goready 将等待接收数据的 goroutine 标记成可运行状态 Grunnable,并把该 goroutine 放到发送方所在处理器的 runnext 上,等待处理器唤醒。

5.3 异步接收

如果sendq为空且channel的缓冲区不为空时,则从缓冲区中接收数据,相应数据从buf中出队

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {​
    ......
    if c.qcount > 0 {
       // 直接从缓冲区接收数据
       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
    }
    ......
}

5.4 阻塞接收

如果sendq中没有待发送的goroutine,且缓冲区为空,则进入阻塞发送逻辑:

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    ......
    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
    }
   
    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)
    
    atomic.Store8(&gp.parkingOnChan, 1)
    gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)
    .....
}

主要流程就是将goroutine休眠与数据等封装为sudog入recvq队列,细节如下:

  • getg 获取发送数据的 goroutine
  • acquireSudog 获取 sudog 结构
  • 将创建并初始化的 sudog 加入recvq,并设置到当前 goroutine 的 waiting上,表示 goroutine 正在等待该 sudog 准备就绪
  • gopark 将当前的 goroutine 陷入沉睡等待唤醒

被调度器唤醒后完成阻塞接收,之后进行参数检查,解除channel的绑定并释放sudog,代码如下:

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {​
    ......
    // someone woke us up
    if mysg != gp.waiting {
      throw("G waiting list is corrupted")
    }
    gp.waiting = nil
    gp.activeStackChans = false
    if mysg.releasetime > 0 {
      blockevent(mysg.releasetime-t0, 2)
    }
    success := mysg.success
    gp.param = nil
    mysg.c = nil
    releaseSudog(mysg)
    return true, success
}

关闭channel

关闭channel的操作如下:

close(ch)

以上代码的编译结果如下:go.godbolt.org/z/v51sf4TPc…

与close相关的CALL操作如下:

 CALL    runtime.closechan(SB)

接下来我们对closechan函数进行详细介绍:

代码位置:src/runtime/chan.go

closechan()函数的主要逻辑可以分为三部分:

  • 异常检查 :首先对异常进行检查,判断channel是否符合close的状态
  • 释放sudog :分别从recvq和sendq中回收等待的接收者和发送者,加入glist(待清除队列)中
  • 调度goroutine :为glist中的所有阻塞goroutine触发调度

6.1 异常检查

closechan()首先对channel进行异常检查:

func closechan(c *hchan) {
    if c == nil {
      panic(plainError("close of nil channel"))
    }
    
    lock(&c.lock)
    if c.closed != 0 {
      unlock(&c.lock)
      panic(plainError("close of closed channel"))
    }
    
    c.closed = 1
    ......
}
  • 判断channel是否是nil或者已经关闭
    • 关闭nil channel或者已经关闭的channel都会引发panic
  • 将channel状态标记为close

6.2 释放sudog

异常检查之后释放所有阻塞在sendq和recvq中的sudog:

func closechan(c *hchan) {
    ......
    var glist gList

    // release all readers
    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)
    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)
    ......
}
  • 回收接收者和发送者的sodug,并将其加入到glist中

6.3 调度goroutine

之后调用goready()为glist中所有goroutine触发调度,状态从 _Gwaiting 设置为 _Grunnable 状态,等待调度器的调度。

func closechan(c *hchan) {​
    ......​
    // Ready all Gs now that we've dropped the channel lock.
    for !glist.empty() {
       gp := glist.pop()
       gp.schedlink = 0
       goready(gp, 3)
}

select

golang 中的 select 语句的实现,在 runtime/select.go 文件中,本篇文章并不打算探讨 select 的实现。

我们主要关注通过select操作channel时channel的状态。

7.1 向channel中发送数据

select {
    case c <- x:
        ... 
    default:
        ... 
}

会被编译为:

if selectnbsend(c, v) {
    ... 
} else {
    ... 
}

对应 selectnbsend 函数如下:

func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {
    return chansend(c, elem, false, getcallerpc(unsafe.Pointer(&c)))
}

7.2 从channel中接收数据

select {
    case v = <-c:
        ... 
    default:
        ... 
}

会被编译为:

if selectnbrecv(&v, c) {
    ... 
} else {
    ... 
}

对应 selectnbrecv 函数如下:

func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected bool) {
    selected, _ = chanrecv(c, elem, false)
    return
}

另一种接收数据的方式:

select {
    case v, ok = <-c:
        ... 
    default:
        ... 
}

会被编译为:

if c != nil && selectnbrecv2(&v, &ok, c) {
    ... 
} else {
    ... 
}

对应 selectnbrecv2 函数如下:

func selectnbrecv2(elem unsafe.Pointer, received *bool, c *hchan) (selected bool) {
    // TODO(khr): just return 2 values from this function, now that it is in Go.
    selected, *received = chanrecv(c, elem, false)
    return
}

7.3 总结

我们能看到,channel与select语句结合使用时,底层调用的还是chansendchanrecv函数。

区别就在于:当与select语句结合使用时,是非阻塞调用,而不与select结合使用时,一般是阻塞调用

总结

以上,我们完成了对channel数据结构,发送数据到channel,从channel中接收数据,关闭channel等常见操作的源码解析。

channel的操作并不复杂,结构上围绕着环形缓存和sendq、recvq展开,流程上围绕着上锁/解锁,阻塞/非阻塞,缓冲/非缓冲,缓存入队出队,sudog入队出队,协程休眠/唤醒等操作展开。

有兴趣的同学还可以思考一下社区比较受关注的问题:

  • 如何设计无锁的channel

  • 如何设计无限缓存的channel

本文正在参加技术专题18期-聊聊Go语言框架