Golang-Channel原理解析
本文主要分析golang实现并发基础组件channel的实现原理;
主要内容分为几个部分
Section1:channel使用实例分析
Section2:源码分析
<h1>Section1 channel使用实例</h1>
channel主要是为了实现go的并发特性,用于并发通信的,也就是在不同的协程单元goroutine之间同步通信。 下面主要从三个方面来讲解: 我们创建channel时候有两种,一种是带缓冲的channel一种是不带缓冲的channel。创建方式分别如下: buffered channel 如果我们创建一个带buffer的channel,底层的数据模型如下图: 当dequeue一个元素时候,如下所示: 那么还有一个问题,当我们新建channel的时候,底层创建的hchan数据结构是在哪里分配内存的呢?其实Section2里面源码分析时候已经做了分析,hchan是在heap里面分配的。 如下图所示: 不同goroutine在channel上面进行读写时,涉及到的过程比较复杂,比如下图: G1作用于底层hchan的流程如下图: G2读取时候作用于底层数据结构流程如下图所示: 上面的读写思路其实很简单,除了hchan数据结构外,不要通过共享内存去通信;而是通过通信(复制)实现共享内存。 写入满channel的场景 如下图所示:channel写入3个task之后队列已经满了,这时候G1再写入第四个task的时候会发生什么呢? 这个地方需要介绍一下Golang的scheduler的。我们知道goroutine是用户空间的线程,创建和管理协程都是通过Go的runtime,而不是通过OS的thread。 但是Go的runtime调度执行goroutine却是基于OS thread的。如下图: 具体关于golang的scheduler的原理,可以看前面的一篇博客,关于go的scheduler原理分析。 当向已经满的channel里面写入数据时候,会发生什么呢?如下图: 上图流程大概如下: 所以整个过程中,OS thread会一直处于运行状态,不会因为协程G1的阻塞而阻塞。最后当前的G1的引用会存入channel的sender队列(队列元素是持有G1的sudog)。 那么blocked的G1怎么恢复呢?当有一个receiver接收channel数据的时候,会恢复 G1。 实际上hchan数据结构也存储了channel的sender和receiver的等待队列。数据原型如下: 这个时候,如果G1进行一个读取channel操作,读取前和读取后的变化图如下图: 整个过程如下: 这个时候将G1恢复到可运行状态需要scheduler的参与。G2会调用goready(G1)来唤醒G1。流程如下图所示: 读取空channel的场景 当channel的buffer里面为空时,这时候如果G2首先发起了读取操作。如下图: 这个时候,如果有一个G1执行读取操作,最直观的流程就是: 但是我们有更加智能的方法:direct send; 其实也就是G1直接把数据写入到G2中的elem中,这样就不用走G2中的elem复制到buffer中,再从buffer复制给G1。如下图: 具体过程就是G1直接把数据写入到G2的栈中。这样 G2 不需要去获取channel的全局锁和操作缓冲。 (1)goroutine-safe (2)store values, pass in FIFO. (3)can cause goroutines to pause and resume. (4)channel的高性能所在: 在源码<code>runtime/chan.go</code> 里面定义了channel的数据模型,channel可以理解成一个缓冲队列,这个缓冲队列用来存储元素,并且提供FIFO的语义。源码如下: channel的数据结构相对比较简单,主要是两个结构: 我们新建一个channel的时候一般使用 <code>make(chan, n)</code> 语句,这个语句的执行编译器会重写然后执行 chan.go里面的 makechan函数。函数源码如下: 函数接收两个参数,一个是channel里面保存的元素的数据类型,一个是缓冲的容量(如果为0表示是非缓冲buffer),创建流程如下: 所以,整个创建channel的过程还是比较简单的。 所有执行 <code>ep < c</code> 使用ep接收channel数据的代码,最后都会调用到chan.go里面的 <code>chanrecv函数</code>。 函数的定义如下: 从源码注释就可以知道,该函数从channel里面接收数据,然后将接收到的数据写入到ep指针指向的对象里面。 还有一个参数block,表示当channel无法返回数据时是否阻塞等待。当block=false并且channel里面没有数据时候,函数直接返回(false,false)。 接收channel的数据的流程如下: 所有执行 <code>c < ep</code> 将ep发送到channel的代码,最后都会调用到chan.go里面的 <code>chansend函数</code>。 函数的定义如下: 函数有三个参数,第一个代表channel的数据结构,第二个是要指向写入的数据的指针,第三个block代表写入操作是否阻塞。 向channel写入数据主要流程如下: 当我们执行channel的close操作的时候会关闭channel。 关闭的主要流程如下所示: 您可能感兴趣的文章:
当我们向channel里面写入数据时候,会直接把数据存入circular queue(send)。当Queue存满了之后就会是如下的状态:
从上图可知,recvx自增加一,表示出队了一个元素,其实也就是循环数组实现FIFO语义。
当我们使用make去创建一个channel的时候,实际上返回的是一个指向channel的pointer,所以我们能够在不同的function之间直接传递channel对象,而不用通过指向channel的指针。
上图中G1会往channel里面写入数据,G2会从channel里面读取数据。
G1这时候会暂停直到出现一个receiver。
等待队列里面是sudog的单链表,sudog持有一个G代表goroutine对象引用,elem代表channel里面保存的元素。当G1执行<code>ch<-task4</code>的时候,G1会创建一个sudog然后保存进入sendq队列,实际上hchan结构如下图:
会创建一个sudog,将代表G2的sudog存入recvq等待队列。然后G2会调用gopark函数进入等待状态,让出OS thread,然后G2进入阻塞态。
hchan mutex
copying into and out of hchan buffer
a)hchan sudog queues
b)calls into the runtime scheduler (gopark, goready)
a)调用runtime scheduler实现,OS thread不需要阻塞;
b)跨goroutine栈可以直接进行读写;
1)一个数组实现的环形队列,数组有两个下标索引分别表示读写的索引,用于保存channel缓冲区数据。
2)channel的send和recv队列,队列里面都是持有goroutine的sudog元素,队列都是双链表实现的。
3)channel的全局锁。
【文末有惊喜!】一文读懂golang channel
golang的select实现原理剖析
golang 切片截取 内存泄露_怎么看待Goroutine 泄露
图解Go的channel底层原理
golang channel的使用以及调度原理
Golang-Channel原理解析
Go range实现原理及性能优化剖析
golang chan原理
Golang Channel原理
golang基础教程