Go Channel 数据结构

77 阅读4分钟

先抛出一个问题:讲讲go中的channel,这个时候怎么回答?

  1. 首先channel是go中的一种数据类。
  2. 用于协程之间的通讯。
  3. 可以用于传递信号。
  4. 也可以批量收集一些程序执行的结果集。

当然,我的应用比较多的场景就是当作一个程序进行内部的队列使用,或者是传递信号。

直接上代码look:

// 这个是底层的数据机构
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 // 锁 保证并发安全
}
  • qcount:表示当前channel中存在多少个元素
  • dataqsiz:丹铅channel能存放多少个元素
  • buf:channel中用于存放元素的环形缓冲区
  • elemsize:channel元素类型的大小
  • closed:用于标识channel是否关闭
  • elemtype:channel中存放元素的类型
  • sendx:发送元素进入环形缓冲区index,下一个发送元素的位置(索引)
  • recvx:接收元素所在的环形缓冲区的index,下一个接收元素的位置
  • recvq:接收goroutine的队列|因接收而陷入阻塞的协程队列
  • sendq:发送goroutine的队列|因发送而陷入阻塞的协程队列

来看看recvq、sendq的waitq结构look:

type waitq struct {
    first *sudog // 队列的头部
    last  *sudog // 队列的尾部
}


type sudog struct {
    // The following fields are protected by the hchan.lock of the
    // channel this sudog is blocking on. shrinkstack depends on
    // this for sudogs involved in channel ops.

    g *g // 关联的goroutine

    next *sudog //队列中的下一个 sudog(链表结构),队列的下一个节点
    prev *sudog // 队列中的上一个 sudog 对垒的上一个节点
    // 指向用户传递过来的数据的内存地址 
    elem unsafe.Pointer // data element (may point to stack)

    // The following fields are never accessed concurrently.
    // For channels, waitlink is only accessed by g.
    // For semaphores, all fields (including the ones above)
    // are only accessed when holding a semaRoot lock.

    acquiretime int64 // 获取锁的时间戳
    releasetime int64 // 解锁的时间戳
    ticket      uint32 // 调试或统计

    // isSelect indicates g is participating in a select, so
    // g.selectDone must be CAS'd to win the wake-up race.
    isSelect bool // 是否有select进行监听

    // success indicates whether communication over channel c
    // succeeded. It is true if the goroutine was awoken because a
    // value was delivered over channel c, and false if awoken
    // because c was closed.
    success bool // 通信是否成功

    // 二叉树的父节点
    parent   *sudog // semaRoot binary tree 
    // g.waiting 列表或 semaRoot
    waitlink *sudog // g.waiting list or semaRoot
    // semaRoot 的尾部
    waittail *sudog // semaRoot
    // 关联当前的channel
    c        *hchan // channel
}

其实channel的底层的数据结构也是数组,我们在make的时候buf其实就是存储的指向一个数组的指针。channel巧妙的使用sendx、recvx这两个字段完成一个闭环实现一个环状的环形数组。snedx是当前写入的索引,recvx是当前读的索引,都会进行+1和-1操作。

ch := make(chan int,5) // 创建一个缓冲区位5的channel
索引:0   1   2   3   4

初始状态
qcount = 0
sendx = 0
recvx = 0

sendx = (sendx + 1) % dataqsiz

这个时候写入数据
第一次写:
ch <- 10
这个时候
buf[sendx] = buf[0]
sendx = (0+1) % 5 = 1
qoucnt = 1
qcount++

第二次写:
ch <- 20
buf[sendx] = buf[1]

sendx = (sendx+1) % 5 = 2 --> sendx = (1+1) % 5 = 2 
sendx = 2
qcount++


看看读数据:
第一次读:
x := <- ch
buf[recvx] = buf[0] = 10
recvx = (recvx + 1) % 5 = 1
qcont--

第二次读
x := <- ch
buf[recvx] = buf[1] = 10
这个时候recvx = 1 因为第一次读过,位置在第一个
recvx = (recvx + 1) % 5 = 2
qcont--



注意:注意当前写到最后一个位置的之后,此时就是 (4+1) % 5 = 0 这个时候sendx回到原点,读区也是一样,这个时候就完成了一个闭环,形成一个环形数组。
当前我们定的初始容量是5,如果没有读的情况下,插入第六个的时候就会发生阻塞。

接下来聊聊阻塞: 写阻塞:如果创建一个有缓冲区的chan,当插入的数据已经占满缓冲区的时候,这个时候就会挂起并阻塞,直到缓冲区有空间才能进行写入。
读阻塞:如果再读的时候发生阻塞,也是一样,如果发生阻塞,直接会被挂起阻塞,等待被唤醒。

现在梳理一下,读的时候会阻塞的原因:

  • 使用了一个无缓冲的channel
  • 缓冲区没有数据可以读
  • channel已经被关闭
  • 所有的select case 阻塞并且没有设置default
  • 读取一个nil channel

写流程的阻塞原因:

  • 使用了一个无缓冲channel
  • 缓冲区已经满了
  • 写入一个nil的channel
  • 所有的select case 阻塞并且没有设置default