【WIP】Golang 理解channel源码

这期介绍关于golang channel的内部工作原理,从源码入手结合资料介绍channel作为goroutine的通信机制创建,写入,读出的过程。文中截图均源自或改编自kavya的understanding channels. 需要基本的goroutine知识,以及channel使用方法为基础来理解。

一,channel数据结构

在golang语言设计中,goroutine可以独立并行的执行任务,channel负责goroutine之间的交互和同步。

channel内部由一个hchan的数据结构构成:

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     mutex
}

除了各种容量参数外,buf是一个指向在堆中初始化的内存的指针,代表一个存储元素的队列;

elemtype是我们初始化channel存放的类型

sendq和recvq分别是存储发送和等待goroutine task的队列,内部由sudog栈来维护

sendx,recvx记录发送和接收的索引

lock为互斥锁保证channel读写的线程安全

 

二,channel创建

在hchan数据结构中我们看到,channel中是有buf数据结构的,这反映在我们make channel的方式中:

ch = make(chan int)    // unbuffered channel
ch = make(chan int, 0) // unbuffered channel
ch = make(chan int, 3) // buffered channel with capacity 3

 

 

当make chan时其实是在堆中分配了一个hchan结构,并返回其指针指向堆地址空间以便于使用:

【WIP】Golang 理解channel源码_第1张图片

 

此时,channel中会分配使用hchan结构,我们看到hchan结构的buf是一个环形队列,需要sendx和recvx记录发送和接收的位置,并按照声明参数来初始化大小和存放数据的类型。:

【WIP】Golang 理解channel源码_第2张图片

 

三, channel读写

当由goroutine向channel中写数据时,

1. 该goroutine获取hchan中的互斥锁

2. 把任务复制进到buf中的队列中,并sendx加一

3. 释放互斥锁

【WIP】Golang 理解channel源码_第3张图片

 

当有goroutine向channel中读出数据时,

1. 获取hchan结构中的锁

2. hchan中buf队列出队,并返回给goroutine

3. 释放hchan的锁

【WIP】Golang 理解channel源码_第4张图片

这种方式避免了进程间通信大量的共享内存,而是用hchan结构进行交互替代

 

这个时候产生问题:

如果是无缓存channel或者goroutine使用channel读写超过了buf初始容量,该怎么办?

这里讲到goroutine其实是更轻量的用户级线程,即被golang的运行期创建管理,而不是内核态线程。对应到内核态线程可能是m对n的形式,一个内核态线程对应管理多个goroutine的任务。内核态线程通过context去管理goroutine,保证可执行goroutine的序列。

其中M是操作系统的线程,G是用户启动的goroutine,P是与调度相关的context,每个M都拥有一个P,P维护了一个能够运行的goutine队列,用于该线程执行。

【WIP】Golang 理解channel源码_第5张图片

 

sendq队列中维护了sudog数据结构由栈组成,存放goroutine同步的必要信息和存放元素。goroutine等待状态时,操作scheduling context而不影响内核态线程。当buf中移出元素时buf队列出队,sudog栈pop出元素,buf进队列。

【WIP】Golang 理解channel源码_第6张图片

当sudog pop元素时,我们可以利用运行期栈空间质相邻特点把G1空间写到G2空间中,这样直接发送的方式避免了切换开销

【WIP】Golang 理解channel源码_第7张图片

这里buf是一段放数据用的内存,锁是用的结构体里的mutex,并且进入send函数之后就会上锁了,之后才会考虑是放入buf还是sudog

【WIP】Golang 理解channel源码_第8张图片

总结:buf是一个golang存放缓存任务的队列,当队列满时,会在sendq中创建sudog结构并存放在goroutine的栈空间内,当前goroutine状态改为等待并通过内核态的OS thread的scheduler context管理做到阻塞goroutine而不影响OS thread。当buf中任务被消费出现空缺,buf出队列,sudog中goroutine唤醒,并地址扩展到当前goroutine,buf进队列。

 

你可能感兴趣的:(后台相关)