先抛出一个问题:讲讲go中的channel,这个时候怎么回答?
- 首先channel是go中的一种数据类。
- 用于协程之间的通讯。
- 可以用于传递信号。
- 也可以批量收集一些程序执行的结果集。
当然,我的应用比较多的场景就是当作一个程序进行内部的队列使用,或者是传递信号。
直接上代码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