大多数的编程语言的并发编程模型是基于线程和内存同步,而Golang 的并发编程的模型则用 goroutine 和 channel 来替代,goroutine用于执行并发任务,channel用于并发控制以及goroutine的通信。这次跟随一个demo探索一下channel底层的奥秘。

channel数据结构:

type hchan struct {
 // chan里元素数量
 qcount   uint
 // chan维护的数组的长度
 dataqsiz uint
 // 维护的数组的指针
 buf      unsafe.Pointer
 // chan中元素大小
 elemsize uint16
 // chan是否被关闭的标志
 closed   uint32
 // chan 中元素类型
 elemtype *_type
 // 已发送元素在循环数组中的索引
 sendx    uint
 // 已接收元素在循环数组中的索引
 recvx    uint
 // 等待接收的goroutine队列
 recvq    waitq
 // 等待发送的goroutine队列
 sendq    waitq
 // 保证对chan的读写是原子操作
 lock mutex
}

数据结构图:

image.png

示例代码:

说明:channel容量为3,设置六个发送,构造goroutine被阻塞状态,八个接收,构造接收的G阻塞状态场景,便于探秘channel运作过程。

func main() {
 ch := make(chan int, 3)
 CheckChannel(ch)
}
func CheckChannel(ch chan int) {
 _//6个发送_ go func() {ch <- 1}()
 go func() {ch <- 2}()
 go func() {ch <- 3}()
 go func() {ch <- 4}()
 go func() {ch <- 5}()
 go func() {ch <- 6}()
 _//8个接收_ go func() {<- ch}()
 go func() {<- ch}()
 go func() {<- ch}()
 go func() {<- ch}()
 go func() {<- ch}()
 go func() {<- ch}()
 go func() {<- ch}()
 go func() {<- ch}()
 time.Sleep(time.Second * 5)
 fmt.Println("stop")
}

调试阶段分析

就缓冲性channel来debug该代码

首先缓冲型:当执行到CheckChannel时,已经初始化了一块内存。

image.png

打开dehug界面查看该数据结构:

image.png

此时刚初始化,底层数组的元素数量为0。发送索引与接收索引都指向数组索引0,发送与接收队列都没有G存在。

一个元素发送

接下来step over,执行完第一个G,观察debug

image.png

观察红框内容,看到底层数组的0位置接收到了第一个groutine的数据,底层的sendx指针指向了数组的索引1,表示该再次发送数据会被数组索引1接收,recvx为0,说明有G接收数据时会接收数组索引0处的数据。

此时结构:

image.png

发送阻塞

继续step over,到发送数据结束,查看debug:

image.png

可以看到由于没有接收者,底层数组里面已经塞满了,查看sendx和recvx的值都为0,说明如果有了接收者就取出索引0处的数据。有发送者就会把数据拷贝到0处(如果0处数据被取出)。

另外查看sendq,可以查看里面已经积压了G队列,这是由于底层数组已塞满,channel会创建一个sudog数据结构,获取G的指针,并将G放入自己的等待队列,此时的积压G处于Gwaiting状态,既不在全局运行队列,也不在某个P(调度器)的运行队列,等待有接受者接收数据,触发goready函数使该G进入可调度即Grunnable状态:

image.png

可以看出,该结构里面积压了三个G,通过next连接下一个G,当然也有pre指针连接前一个G,到第一个或最后一个G的指针指向一个nil。

此时结构:

image.png

接收第一个元素

继续step over到第一个接收的G,观察该channel结构:

image.png

可以看到,索引0处的1已经被接收,由于有了接收者,等待队列中的G被唤醒,进入可调度即Grunnable状态,调度执行结束后被释放。而积压在sendq的队列第二个G作为了队首,并且sendx和recvx指向索引1,channel发送数据和接收数据都会在数组索引1进行。

查看sendq中的G队列:

image.png

可以看到队首的groutine已经被释放,队列中只剩两个G;

image.png

第一批接收结束

继续step over,到第三个接收G执行结束:

image.png

可以看到数组中的元素全部变成了第一次积压在sendq中的G要发送的元素,而且由于已经发送,积压的G全部被释放,索引指针全部指向了0。

image.png

数组开始有空闲位

继续step over一下,观察sendx与recvx的指针位置:

image.png

可以看到,由于没有发送方,导致sendx的指针指向索引0,recvx则后移,对剩余数组的元素进行赋值,此时已经没有积压的G,来一个接收者释放一个索引位。

此时的数据结构:

image.png

数组已无元素

继续step over,到第六个接收G结束:
image.png

恢复到初始的效果了,没有元素,没有积压的G。

此时数据结构如图:

image.png

接收阻塞

继续step over创建接收G,到创建G结束,观察:

image.png

recvq接收队列中有两个积压的G被阻塞住,陷入Gwaiting状态,由于程序后面不会有发送者,所以会一直阻塞到主协程退出。

此时数据结构:

image.png

调试结束

继续执行,主协程睡眠五秒,退出,子协程全部退出。

对于非缓冲型的channel,则是直接把值从发送的G拷贝到接收的G。

调试总结

说到底,通过channel传递消息就是值的拷贝,有缓冲的channel先把发送方G的值拷贝到自己维护的数组,再拷贝到接收G,而非缓冲型的则直接从发送栈数据拷贝到接收栈空间。

最后贴一个图:

image.png

image.png

原创文章,转载请说明出处。


郭朝
24 声望7 粉丝