0%

golang的channel底层结构

1. 解析

img

1.1 源码

源码路径: https://github.com/golang/go/blob/master/src/runtime/chan.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type hchan struct {
qcount uint // 队列中所有数据总数
dataqsiz uint // 环形队列的 size
buf unsafe.Pointer // 指向 dataqsiz 长度的数组
elemsize uint16 // 元素大小
closed uint32
elemtype *_type // 元素类型
sendx uint // 已发送的元素在环形队列中的位置
recvx uint // 已接收的元素在环形队列中的位置
recvq waitq // 接收者的等待队列
sendq waitq // 发送者的等待队列

// lock 锁保护 hchan 中的所有字段,以及此通道上被阻塞的 sudogs 中的多个字段。持有 lock 的时候,禁止更改另一个 G 的状态(特别是不要使 G 状态变成ready),因为这会因为堆栈 shrinking 而发生死锁。
lock mutex // 锁
}


type waitq struct {
first *sudog
last *sudog
}
  1. 有一个循环数组,存放有缓冲的channel 数据,size是有缓冲的大小,并且有2个index表示索引值。
  2. 有两个goroutine 等待队列,一个是接受者等待队列(recvq),和发送者等待队列(sendq),存放sudog。sudog 代表了一个在等待队列中的 goroutine
  3. 有个锁保证读写 channel 的操作都是原子的,发送和接受数据都会加锁。

1.2 流程

1
ch := make(chan Task, 3)

创建channel实际上就是在内存中实例化了一个hchan的结构体,并返回一个ch指针。

1. 发送数据流程

ch <-task0

ch <-task0

ch <-task0

可以看到sendx 从 0 变成 2,因为是循环数组,满了又变成了0

1

2. 满了再发送阻塞

当G1 向buf已经满了的 channel 发送数据的时候,调度器会将G1的状态设置为waiting,当G1变为waiting状态后,会创建一个代表自己的sudog的结构,然后放到sendq这个list中。

img

3. 其他协程取出数据

首先 G2 读取元素之后,将 G1 的状态变为 goready。

img

然后 G1 从原先的 waiting 状态变为 runnable 状态,然后重新放入 P 的本地队列中,等待调度。

img

2. 头脑风暴

  • 底层只有一个环形数组,有缓冲的channel才有,存放数据。两个index,一个sendx,一个recvx。
  • 有一把锁,保护读 channel 或写 channel 的操作都是原子的。
  • 两个双向链表队列,一个sendq,一个recvq,上面存着阻塞的G化身的sudog。
  • 如果G阻塞,主动调用Go的调度器,G会抽象成一个代表自己的 sudog,挂在上面的队列上。

3. 参考资料

给作者打赏,可以加首页微信,咨询作者相关问题!