大多数的编程语言的并发编程模型是基于线程和内存同步,而Golang 的并发编程的模型则用 goroutine 和 channel 来替代,groutine用于执行并发任务,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
}
可将其抽象化为下图,便于理解。
示例代码
/*
示例说明: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时,已经初始化了一块内存。
打开dehug界面查看该数据结构:
此时刚初始化,底层数组的元素数量为0。发送索引与接收索引都指向数组索引0,发送与接收队列都没有G存在。
1、一个元素发送
接下来step over,执行完第一个G,观察debug
观察红框内容,看到底层数组的0位置接收到了第一个groutine的数据,底层的sendx指针指向了数组的索引1,表示该再次发送数据会被数组索引1接收,recvx为0,说明有G接收数据时会接收数组索引0处的数据。此时结构如下图:
2、发送阻塞
继续step over,到发送数据结束,查看debug:
可以看到由于没有接收者,底层数组里面已经塞满了,查看sendx和recvx的值都为0,说明如果有了接收者就取出索引0处的数据。有发送者就会把数据拷贝到0处(如果0处数据被取出)。
另外查看sendq,可以查看里面已经积压了G队列,这是由于底层数组已塞满,channel会创建一个sudog数据结构,获取G的指针,并将G放入自己的等待队列,此时的积压G处于Gwaiting状态,既不在全局运行队列,也不在某个P(调度器)的运行队列,等待有接受者接收数据,触发goready函数使该G进入可调度即Grunnable状态。
可以看出,该结构里面积压了三个G,通过next连接下一个G,当然也有pre指针连接前一个G,到第一个或最后一个G的指针指向一个nil。此时结构如下图:
3、接收第一个元素
继续step over到第一个接收的G,观察该channel结构:
可以看到,索引0处的1已经被接收,由于有了接收者,等待队列中的G被唤醒,进入可调度即Grunnable状态,调度执行结束后被释放。而积压在sendq的队列第二个G作为了队首,并且sendx和recvx指向索引1,channel发送数据和接收数据都会在数组索引1进行。
查看sendq中的G队列:
可以看到队首的groutine已经被释放,队列中只剩两个G;
4、第一批接收结束
继续step over,到第三个接收G执行结束:
可以看到数组中的元素全部变成了第一次积压在sendq中的G要发送的元素,而且由于已经发送,积压的G全部被释放,索引指针全部指向了0。
5、数组开始有空闲位
继续step over一下,观察sendx与recvx的指针位置:
可以看到,由于没有发送方,导致sendx的指针指向索引0,recvx则后移,对剩余数组的元素进行赋值,此时已经没有积压的G,来一个接收者释放一个索引位。
此时的数据结构:
6、数组已无元素
继续step over,到第六个接收G结束
恢复到初始的效果了,没有元素,没有积压的G。
此时数据结构如图:
7、接收阻塞
继续step over创建接收G,到创建G结束,观察:
recvq接收队列中有两个积压的G被阻塞住,陷入Gwaiting状态,由于程序后面不会有发送者,所以会一直阻塞到主协程退出。
此时数据结构:
8 调试结束
继续执行,主协程睡眠五秒,退出,子协程全部退出。
对于非缓冲型的channel,则是直接把值从发送的G拷贝到接收的G。
调试总结
说到底,通过channel传递消息就是值的拷贝,有缓冲的channel先把发送方G的值拷贝到自己维护的数组,再拷贝到接收G,而非缓冲型的则直接从发送栈数据拷贝到接收栈空间。
最后贴一个图