[Golang 修仙之路] Go基础:channel

23 阅读9分钟

本文章仅供个人学习使用。

参考

  1. 秀才:golangstar.cn/go_series/g…

底层实现

数据结构

  1. Go的channel就是直接在堆上创建一个hchan,make会返回一个指向hchan的指针。
  2. hchan有几个核心字段,分别是:一把锁、一个环形缓冲区、环形缓冲区的两个指针sendx和recvx、两个队列sendq和recvq。

完整字段如下:

type hchan struct {
   qcount   uint           // 循环队列中的数据总数
   dataqsiz uint           // 循环队列大小
   buf      unsafe.Pointer // 指向循环队列的指针
   elemsize uint16         // 循环队列中的每个元素的大小
   closed   uint32         // 标记位,标记channel是否关闭
   elemtype *_type         // 循环队列中的元素类型
   sendx    uint           // 已发送元素在循环队列中的索引位置
   recvx    uint           // 已接收元素在循环队列中的索引位置
   recvq    waitq          // 等待从channel接收消息的sudog队列
   sendq    waitq          // 等待向channel写入消息的sudog队列
   lock mutex              // 互斥锁,对channel的数据读写操作加锁,保证并发安全
}
  1. sendq 和 recvq 是 sudog组成的双向链表:
type waitq struct {
    first *sudog              // sudog队列的队头指针
    last  *sudog              // sudog队列的队尾指针
}

sudog 的结构是这样:

type sudog struct {
   g *g                  // 绑定的goroutine
   next *sudog           // 指向sudog链表中的下一个节点
   prev *sudog           // 指向sudog链表中的下前一个节点
   elem unsafe.Pointer   // 数据对象
   acquiretime int64     
   releasetime int64
   ticket      uint32
   isSelect bool
   success bool
   parent   *sudog // semaRoot binary tree
   waitlink *sudog // g.waiting list or semaRoot
   waittail *sudog // semaRoot
   c        *hchan // channel
}

channel 的初始化

初始化函数有一个参数是初始化的类型,分三种:

  1. 无缓冲区的chan,则直接在堆上为hchan结构体分配内存。
  2. 有缓冲区 && 元素类型不含指针,则直接在堆上为 hchan + buf 分配一段连续的内存。
  3. 有缓冲区 && 元素类型包含指针,则分配两次内存,先给hchan分配内存,再给buf数组分配内存。
func makechan(t *chantype, size int) *hchan {
   elem := t.elem
   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))
   // 如果内存超了,或者分配的内存大于channel最大分配内存,或者分配的size小于0,直接Panic
   if overflow || mem > maxAlloc-hchanSize || size < 0 {
      panic(plainError("makechan: size out of range"))
   }

   var c *hchan
   switch {
   case mem == 0:
      // 没有缓冲区buf,只分配hchan这个结构的内存,不分配buf的内存
      c = (*hchan)(mallocgc(hchanSize, nil, true))
      // Race detector uses this location for synchronization.
      c.buf = c.raceaddr()
   case elem.ptrdata == 0:    // 有缓冲区buf,元素类型不含指针,为当前的 hchan结构和buf数组分配一块连续的内存空间
      c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
      c.buf = add(unsafe.Pointer(c), hchanSize)
   default:
      // 有缓冲区,且元素包含指针类型,hchan结构和buf数组各自分配内存,分两次分配内存
      c = new(hchan)
      c.buf = mallocgc(mem, elem, true)
   }

   c.elemsize = uint16(elem.size)
   c.elemtype = elem
   c.dataqsiz = uint(size)
   lockInit(&c.lock, lockRankHchan)

   if debugChan {
      print("makechan: chan=", c, "; elemsize=", elem.size, "; dataqsiz=", size, "\n")
   }
   return c
}

向channel中写入

你看发送和接收函数,都有一个参数叫block,标识了是阻塞模式还是非阻塞模式。

  • 阻塞模式是指:正常使用chan,比如 <- ch
  • 非阻塞模式是指:select 多路复用。

首先判断当前chan是否为nil,为nil,则调用gopark()阻塞。

加锁。

判断chan是否已经close,如果是,则panic。

向channel中写入分3种情况:

  1. 如果有阻塞等待的接收者,则不经过缓冲区,直接把待发送的数据copy到接收处。
  2. 没有阻塞等待的接收者,则判断缓冲区是否有空闲空间,如果有,则把数据写入缓冲区,并更新sendxqcount.
  3. 如果缓冲区已经满了,则将当前的groutine和要发送的数据封装成一个sudog,将sudog加入到sendq,最后调用gopark将当前goroutine挂起(阻塞)。

解锁。

源码如下:

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
   if c == nil {    // channel=nil,当前goroutine会被挂起
      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, abi.FuncPCABIInternal(chansend))
   }
   // 非阻塞,channel未关闭且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
   // 什么意思? 就是当前 channel 有正在阻塞等待的接收方,就是之将数据由一个goroutine发往另一个goroutine
   // 直接将待发送数据直接copy到接收处
   // 直接从一个用一个goroutine操作另一个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).
      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)    // 将元素放在sendx处
      if raceenabled {
         racenotify(c, c.sendx, nil)
      }
      typedmemmove(c.elemtype, qp, ep)
      c.sendx++                    // sendx加1  
      if c.sendx == c.dataqsiz {
         c.sendx = 0
      }
      c.qcount++                    // channel总量加1
      unlock(&c.lock)
      return true
   }
    // 走到这里,说明上述情况为命中,channel已经满了,如果是非阻塞的直接返回,否则需要调用gopack将这个goroutine挂起,等待被唤醒
   if !block {
      unlock(&c.lock)
      return false
   }

   gp := getg()     // 获取发送数据的goroutine
   mysg := acquireSudog()  // 获取sudog 结构
   mysg.releasetime = 0
   if t0 != 0 {
      mysg.releasetime = -1
   }

   mysg.elem = ep  // 设置待发送数据的内存地址
   mysg.waitlink = nil
   mysg.g = gp   // 绑定发送goroutine
   mysg.isSelect = false
   mysg.c = c
   gp.waiting = mysg  // 设置到发送goroutine的waiting上
   gp.param = nil
   c.sendq.enqueue(mysg) //  将mysg这个sudog加入到当前channel的发送等待队列,等待被唤醒
 
   atomic.Store8(&gp.parkingOnChan, 1)
   gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)

   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中读取

首先判断chan是否为nil,为nil则会阻塞,调用gopark().

加锁。

  1. 判断当前是否有发送者等待发送,如果有:
    • 无缓冲区:直接将发送者唤醒,发送者将发送者的数据copy到接收者。
    • 有缓冲区:缓冲区肯定已经满了。这时候将缓冲区中recvx指针处的数据读出,sendx和recvx都+1,唤醒一个发送者将数据发送到缓冲区。
  2. 如果当前没有发送者等待,则尝试从缓冲区中读取。检查 qcount > 0 缓冲区不为空,则读取 buf[recvx] 处的数据, 并更新recvx和qcount。
  3. 如果缓冲区为空,则把当前的goroutine封装成sudog,把sudog加入到recvq队列,最后调用gopark阻塞当前goroutine。

解锁。

源码如下:

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
   if debugChan {
      print("chanrecv: chan=", c, "\n")
   }
   // channel是nil
   if c == nil {
    // 如果是非阻塞模式,直接返回false,false
      if !block {
         return
      }
      // 如果是阻塞模式,调用goprak挂起goroutine,等待被唤醒
      gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
      throw("unreachable")
   }

   // 在非阻塞模式下
   // 如果是非缓冲型channel并且当前channel的等待发送链表为空或者是缓冲型channel并且buf中没有数据
   if !block && empty(c) {
       //  如果chan没有关闭,则返回 false, false
      if atomic.Load(&c.closed) == 0 {
         return
      }
      // 如果channel关闭了,双重检查,看channel是不是无缓冲chan或者是chan中没有数据,如果是则返回 true, false
      if empty(c) {
         if raceenabled {
            raceacquire(c.raceaddr())
         }
         // 清除ep指针中的数据并立刻返回true,false
         if ep != nil {
            typedmemclr(c.elemtype, ep)
         }
         return true, false
      }
   }

   var t0 int64
   if blockprofilerate > 0 {
      t0 = cputicks()
   }

   lock(&c.lock)
   // 如果channel已经关闭,并且chan中没有数据,返回 (true,false)
   if c.closed != 0 && c.qcount == 0 {
      if raceenabled {
         raceacquire(c.raceaddr())
      }
      unlock(&c.lock)
      // // 清除ep指针中的数据并立刻返回true,false
      if ep != nil {
         typedmemclr(c.elemtype, ep)
      }
      return true, false
   }
   // 优先从发送队列中取数据,如果有等待发送数据的groutine,直接从发送数据的goroutine中取出数据
   if sg := c.sendq.dequeue(); sg != nil {   
      // 从当前channel的发送队列对头取出goroutine,说明有等待发送的goroutine
      // 查看recv发现这里有两种情况
      // 1. 如果是非缓冲型channel,那么直接将数据从发送者的栈copy到接收者的栈接收区
      // 2. 如果是缓冲型channel,但是buf已经满了,首先将recvx处的元素拷贝到接收地址,然后将下一个写入元素拷贝到recvx,recvx和sendx都自增1
      // 拷贝完数据以后,唤醒发送队列中的的goroutine,等待调度器调度
      recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
      return true, true
   }
    // 没有等待发送的队列,并且buf中有元素,从channel的缓冲区中接收数据
   if c.qcount > 0 {
      // 直接从缓冲区buf取出数据
      qp := chanbuf(c, c.recvx)
      if raceenabled {
         racenotify(c, c.recvx, nil)
      }
      // 将数据放到目标地址
      if ep != nil {
         typedmemmove(c.elemtype, ep, qp)
      }
      // 清空缓冲队列buf中对应的元素
      typedmemclr(c.elemtype, qp)
      c.recvx++  // 接收索引自增1
      if c.recvx == c.dataqsiz {
         c.recvx = 0
      }
      c.qcount--   // 队列元素数量减1
      unlock(&c.lock)
      return true, true
   }
   // 同步非阻塞模式,直接返回false,false
   if !block {
      unlock(&c.lock)
      return false, false
   }

   // 走到这里说明是阻塞模式
   // 没有任何数据可以获取到,阻塞住当前读goroutine,并加入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
   c.recvq.enqueue(mysg)   // 加入到接收者队列

   atomic.Store8(&gp.parkingOnChan, 1)
   gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)

   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)   // 阻塞的goroutine被唤醒
   return true, success
}

已经关闭的channel

规则

  1. 向其中写入数据会panic
  2. 从中读取数据 && 缓冲区不为空,可以正常读取。
  3. 从中读取数据 && 缓冲区为空,会返回该类型数据的零值(而不是阻塞)。此时,第二个返回值会返回false。
  4. 正确的读已经关闭的缓冲区的方式是for range 读取:
for v := range ch {
    fmt.Println(v) // 会自动退出循环,当 ch 关闭且读空
}
  1. 如果channel为nil,关闭会发生panic。
  2. 如果关闭一个已经关闭的channel,会发生panic。

代码

package main

import "fmt"

func main() {
    ch := make(chan int, 2)
    ch <- 10
    ch <- 20
    close(ch)

    // 还能读出缓冲区里的 10 和 20
    fmt.Println(<-ch) // 10
    fmt.Println(<-ch) // 20

    // 缓冲区空了,再读会返回零值
    fmt.Println(<-ch) // 0

    // 用 v, ok 判断
    v, ok := <-ch
    fmt.Println(v, ok) // 0 false
}

运行结果:

10
20
0
0 false