Go并发编程系列(七)channel通道及其原理-张柏沛IT博客

正文内容

Go并发编程系列(七)channel通道及其原理

栏目:Go语言 系列:Go并发编程系列 发布时间:2021-02-05 13:54 浏览量:216

Go语言中,channel通道用于多个goroutine间通信

 

有关goroutinechannel的基本概念和使用在本人的《Go入门系列  go并发编程之Goroutinechannel》的系列文章中有介绍,下面是传送门。

http://zbpblog.com/blog-231.html

http://zbpblog.com/blog-232.html

http://zbpblog.com/blog-233.html

 

在本篇文章中,本人就不再对goroutine channel的基本使用做重复的介绍和讲解,本篇文章的重点是介绍channel的实现原理和一些补充性的channel用法。下面内容转载自

《Go专家编程》Go channel实现原理剖析

 

1.chan的数据结构

src/runtime/chan.go:hchan定义了channel的数据结构:

 

type hchan struct {

qcount   uint           // 当前队列中剩余元素个数

dataqsiz uint           // 环形队列长度,即可以存放的元素个数

buf      unsafe.Pointer // 环形队列指针

elemsize uint16         // 每个元素的大小

closed   uint32         // 标识关闭状态

elemtype *_type         // 元素类型

sendx    uint           // 队列下标,指示元素写入时存放到队列中的位置

recvx    uint           // 队列下标,指示元素从队列的该位置读出

recvq    waitq          // 等待读消息的goroutine队列

sendq    waitq          // 等待写消息的goroutine队列

lock mutex              // 互斥锁,chan不允许并发读写

}

 

从这个结构体可以看出,一个channel通道由一个环形队列,goroutine队列,互斥锁以及各种状态标识和类型信息组成。

 

 

环形队列

chan内部使用一个环形队列作为其缓冲区,队列长度是make这个chan的时候指定的。

其中

buf成员指向这个环形队列的内存地址

dataqsiz成员表示这个队列的容量

qcount成员表示这个队列剩余的空闲位置个数

sendx和recvx分别是这个环形队列的写指针和读指针(写入的位置和读取的位置)

比较有意思的是sendx和recvx,当goroutine往一个chan发送数据时,sendx写指针就会移动到下一个元素,读指针不变;而从一个chan接收数据时,读指针也会移到下一个元素的位置,写指针不变。例如一个缓冲区长度为4,类型为int的channel,G1负责发送,G2负责接收:

一开始sendx和recvx都指向第一个元素

  

我们看看下面的几种状况:

A. G1一口气发送5个数字(1,2,3,4,5)但G2不接收的情况下,发送前4个数字的时候都一切顺利,但是发送第5个数字的时候,由于缓冲区是个环形队列,因此此时写指针重新回到了第一个元素与recvx重合,在发送5的时候,由于队列已满就会发生阻塞。

  

然后此时G2开始接收2次,recvx会往后移动2个位置,sendx发现缓冲区中又有空余位置后写入5并往下移动1个位置。第一个位置的元素值1会被5覆盖:

 

 

B. G1先发送1,2,3, 然后G2接收4次,如下图

G2读取过的元素在缓冲区中是不会马上被清除的,但是会被循环往复的写操作覆盖,就像上面的例子中51给覆盖掉(环形队列的特性)

 

 

等待的goroutine队列

如果一个goroutine(简称G)在发送的时候被chan(简称C)阻塞,那么这个G就会将状态置为Gwaiting并从当前内核线程(简称M)切换出来,放入到C等待消息的goroutine队列sendq

同理如果一个Gchan接收数据被阻塞的话,这个G会被放到这个C等待消息的goroutine队列recvq

当这些G被唤醒后会重新回到调度器或上下文环境P的可运行G队列。

 

sendqrecvq的数据结构是双向链表。

一般情况下recvqsendq至少有一个为空。只有一个例外,那就是同一个goroutine使用select语句向channel一边写数据,一边读数据。

 

 

 

chan中的锁

我们知道,chan用于为多个goroutine并发通信,因此往chan读写元素就会涉及到并发读写的数据安全问题。而chan中的成员lock就是为了保证chan读写时的并发安全而设的。每一次的读和写操作都会先对lock上锁,使得一个goroutine对这个chan发送或接收数据的同一时刻其他goroutine不能对该chan发送和接收。

 

 

chan读或写数据的简单流程

 

 

关闭chan

关闭channel时会把recvq中的G全部唤醒,并且如果关闭的时候channel中已经没有数据可接收的话,这些G接收到的数据全为nil

关闭channel也会把sendq中的G全部唤醒,但这些Gpanic

 

如果您需要转载,可以点击下方按钮可以进行复制粘贴;本站博客文章为原创,请转载时注明以下信息

张柏沛IT技术博客 > Go并发编程系列(七)channel通道及其原理

热门推荐
推荐新闻