go源码分析-channel

82 阅读5分钟

前言:学习笔记仅供参考,如果有更好的理解或者存在错误欢迎留言探讨。

Go版本:go1.19.1

Channel源码位置:src/runtime/chan.go

1.chan底层数据结构

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 protects all fields in hchan, as well as several
   // fields in sudogs blocked on this channel.
   //
   // Do not change another G's status while holding this lock
   // (in particular, do not ready a G), as this can deadlock
   // with stack shrinking.
   lock mutex
}

type waitq struct {
   first *sudog
   last  *sudog
}
type hchan struct {
   qcount   uint           // chan中当前元素数量
   dataqsiz uint           // chan中元素容量
   buf      unsafe.Pointer //,channel 中用于存放元素的环形缓冲区;指针类型指向dataqsiz元素数组
   elemsize uint16 //chan元素类型大小
   closed   uint32 //标识chan是否关闭
   elemtype *_type // chan元素类型
   sendx    uint   //写入元素的index
   recvx    uint   // 读取元素的index
   recvq    waitq  // 阻塞的读协程队列
   sendq    waitq  // 阻塞的写协程队列
   lock mutex  //锁
}


// 等待goroutine的双向链表结构
type waitq struct {
   first *sudog
   last  *sudog
}
type sudog struct {
   g *g
   next *sudog
   prev *sudog
   elem unsafe.Pointer  // data element (may point to stack)
   isSelect bool //isSelect表示goroutine是否正在参与一个select
   .....
   c        *hchan  // channel
 }

2.channel的创建

func makechan(t *chantype, size int) *hchan {
   //chan元素类型
   elem := t.elem

   // 根据chan元素大小判断chan元素类型是否合法
   if elem.size >= 1<<16 {
      throw("makechan: invalid channel element type")
   }
   //
   if hchanSize%maxAlign != 0 || elem.align > maxAlign {
      throw("makechan: bad alignment")
   }

   //根据chan元素类型的大小和chan的容量计算要分配的内存空间
   mem, overflow := math.MulUintptr(elem.size, uintptr(size))
   if overflow || mem > maxAlloc-hchanSize || size < 0 {
      panic(plainError("makechan: size out of range"))
   }

   // Hchan does not contain pointers interesting for GC when elements stored in buf do not contain pointers.
   // buf points into the same allocation, elemtype is persistent.
   // SudoG's are referenced from their owning thread so they can't be collected.
   // TODO(dvyukov,rlh): Rethink when collector can move allocated objects.
   var c *hchan
   switch {
   //1.内存空间=0,chan为无缓冲通道
   case mem == 0:
      //分配固定内存,大小为hchanSize
      c = (*hchan)(mallocgc(hchanSize, nil, true))
      // Race detector uses this location for synchronization.
      c.buf = c.raceaddr()
   //2.chan元素不包括指针元素
   case elem.ptrdata == 0
      //给chan和buf分配内存空间
      c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
      c.buf = add(unsafe.Pointer(c), hchanSize)
   //3.chan元素包括指针元素
   default:
      // Elements contain pointers.
      c = new(hchan)
      c.buf = mallocgc(mem, elem, true)
   }

   c.elemsize = uint16(elem.size)//设置chan元素类型大小
   c.elemtype = elem             //设置chan元素类型
   c.dataqsiz = uint(size)       //设置chan容量大小
   lockInit(&c.lock, lockRankHchan)//加锁

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

总结

创建chan的时候源码主要做了以下操作: 1.验证chan元素类型合法性 2.计算chan要分配的内存空间 3.初始化hchan的成员 分三种情况: 1.内存空间=0,chan为无缓冲通道 2.chan元素不包括指针元素 3.chan元素包括指针元素

遇到的问题:

进行debug,第一次进来发现size=2 有点奇怪

微信图片_20230515111028.png

最后发现的gcenable()这边有创建一个chan,这部分感觉和GC垃圾回收有关 这部分内容放到后面再看

微信图片_20230515111117.png

3.chan写入数据

3.1写数据的异常判断

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
  //1.如果chan为空 阻塞goroutine
   if c == nil {
      if !block {
         return false
      }
      gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
      throw("unreachable")
   }
   ....
  
lock(&c.lock)
//2.如果chan关闭抛出panic异常
if c.closed != 0 {
   unlock(&c.lock)
   panic(plainError("send on closed channel"))
}

总结:1.如果chan为空 阻塞goroutine。2.如果chan关闭抛出panic异常

3.2写数据时的几种逻辑判断

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
     ....
    lock(&c.lock)
     ....
    //1.存在阻塞的读协程队列,直接将发送的数据发送到读协程
    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).
       //调用goready唤醒阻塞的读协程
       send(c, sg, ep, func() { unlock(&c.lock) }, 3)
       return true
    }
    ....
    
    ....
    //2.无阻塞的读协程,chan的容量dataqsiz还没满---环形
    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)
       c.sendx++//下一个要写入的元素index++
       if c.sendx == c.dataqsiz {//如果已到环形队列队尾,sendx指向开始下标
          c.sendx = 0
       }
       c.qcount++//当前chan元素数量++
       unlock(&c.lock)
       return true
    }
    
    //3.无阻塞的读协程,chan的容量已满,gopark阻塞当前写协程
   
    gp := getg()//获取当前协程
    mysg := acquireSudog()//acquireSudog调用new(sudog)获取sudog
    //设置sudog的成员变量
    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
    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.
    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
    //释放sudog
    releaseSudog(mysg)
    if closed {
       if c.closed == 0 {
          throw("chansend: spurious wakeup")
       }
       panic(plainError("send on closed channel"))
    }
    return true
  

    
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
   if raceenabled {
      if c.dataqsiz == 0 {
         racesync(c, sg)
      } else {
         // Pretend we go through the buffer, even though
         // we copy directly. Note that we need to increment
         // the head/tail locations only when raceenabled.
         racenotify(c, c.recvx, nil)
         racenotify(c, c.recvx, sg)
         c.recvx++
         if c.recvx == c.dataqsiz {
            c.recvx = 0
         }
         c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
      }
   }
   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)
}

总结:

1.存在阻塞的读协程队列,直接将发送的数据发送到接收者

2.无阻塞的读协程,chan的容量dataqsiz还没满---环形,写入数据

3.无阻塞的读协程,chan的容量已满,gopark阻塞当前写协程

3.3流程图

微信图片_20230515161321.png

4.chan读取数据

4.1 chan读取数据时的异常

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    ....
    //1.chan为空,gopark阻塞协程,---deadkock
    if c == nil {
       if !block {
          return
       }
       gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
       throw("unreachable")
    }

  ....
    //加锁
    lock(&c.lock)

    //2.chan未关闭且chan无元素,解锁,返回
    if c.closed != 0 {
       if c.qcount == 0 {
          if raceenabled {
             raceacquire(c.raceaddr())
          }
          unlock(&c.lock)
          if ep != nil {
             typedmemclr(c.elemtype, ep)
          }
          return true, false
       }
       // The channel has been closed, but the channel's buffer have data.
    }

总结

1.chan为空,gopark阻塞协程,---deadkock

2.chan未关闭且chan无元素,解锁,返回

4.2chan读取数据时的几种逻辑判断

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    lock(&c.lock)

    if c.closed != 0 {
       ....
    }else {
       // Just found waiting sender with not closed.
       //1.读数据时有阻塞的写协程,如果缓冲区大小为0,则接收值并唤醒写协程,
       //缓冲区大小不为0,则读取缓冲区头部元素,并将写协程元素写入缓冲区尾部并唤醒写协程
       //解锁返回
       if sg := c.sendq.dequeue(); sg != nil {
          // Found a waiting sender. If buffer is size 0, receive value
          // directly from sender. Otherwise, receive from head of queue
          // and add sender's value to the tail of the queue (both map to
          // the same buffer slot because the queue is full).
          recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
          return true, true
       }
  }

   //2.chan当前元素数量大于0,读取缓冲区头部元素,修改recvx下标qcount数量,解锁
    if c.qcount > 0 {
       // Receive directly from queue
       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
  }
 

    //3.读取时没有阻塞写协程,chan当前元素个数为0
    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)
    // 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.
    atomic.Store8(&gp.parkingOnChan, 1)
    //阻塞读协程
    gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)

    // 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


func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
   if c.dataqsiz == 0 {
      if raceenabled {
         racesync(c, sg)
      }
      if ep != nil {
         // copy data from sender
         recvDirect(c.elemtype, sg, ep)
      }
   } else {
      // Queue is full. Take the item at the
      // head of the queue. Make the sender enqueue
      // its item at the tail of the queue. Since the
      // queue is full, those are both the same slot.
      qp := chanbuf(c, c.recvx)
      if raceenabled {
         racenotify(c, c.recvx, nil)
         racenotify(c, c.recvx, sg)
      }
      // copy data from queue to receiver
      if ep != nil {
         typedmemmove(c.elemtype, ep, qp)
      }
      // copy data from sender to queue
      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)
   sg.success = true
   if sg.releasetime != 0 {
      sg.releasetime = cputicks()
   }
   goready(gp, skip+1)
}

总结:

1.读数据时有阻塞的写协程,如果缓冲区大小为0,则接收值并唤醒写协程, 缓冲区大小不为0,则读取缓冲区头部元素,并将写协程元素写入缓冲区尾部并唤醒写协程,解锁返回

2.chan当前元素数量大于0,读取缓冲区头部元素,修改recvx下标qcount数量,解锁

3.读取时没有阻塞写协程,chan当前元素个数为0,解锁,阻塞读协程

4.3流程图

微信图片_20230515162038.png

5.关闭chan

func closechan(c *hchan) {
   //1.chan为空,抛出panic异常
   if c == nil {
      panic(plainError("close of nil channel"))
   }

   //加锁
   lock(&c.lock)
   //2.chan已经关闭,解锁,抛出panic异常
   if c.closed != 0 {
      unlock(&c.lock)
      panic(plainError("close of closed channel"))
   }

   if raceenabled {
      callerpc := getcallerpc()
      racewritepc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(closechan))
      racerelease(c.raceaddr())
   }

   
   c.closed = 1

   var glist gList

   // release all readers
   //3.阻塞的读协程添加到glist
   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)
   //4.所有阻塞的写协程添加到glist
   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)

   // Ready all Gs now that we've dropped the channel lock.
   5.唤醒glist中的所有协程
   for !glist.empty() {
      gp := glist.pop()
      gp.schedlink = 0
      goready(gp, 3)
   }
}

总结

1.chan为空,抛出panic异常,加锁

2.chan已经关闭,解锁,抛出panic异常,将chan状态设成关闭

3.阻塞的读协程添加到glist

4.所有阻塞的写协程添加到glist

5.唤醒glist中的所有协程

gList是通过g.s edlink链接的协程的列表