图解Go的channel底层原理
废话不多说,直奔主题。
channel的整体结构图
简单说明:
buf
是有缓冲的channel所特有的结构,用来存储缓存数据。是个循环链表sendx
和recvx
用于记录buf
这个循环链表中的发送或者接收的indexlock
是个互斥锁。recvq
和sendq
分别是接收(<-channel)或者发送(channel <- xxx)的goroutine抽象出来的结构体(sudog)的队列。是个双向链表
源码位于/runtime/chan.go
中(目前版本:1.11)。结构体为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 protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
}
下面我们来详细介绍hchan
中各部分是如何使用的。
先从创建开始
我们首先创建一个channel。
ch := make(chan int, 3)
创建channel实际上就是在内存中实例化了一个hchan
的结构体,并返回一个ch指针,我们使用过程中channel在函数之间的传递都是用的这个指针,这就是为什么函数传递中无需使用channel的指针,而直接用channel就行了,因为channel本身就是一个指针。
channel中发送send(ch <- xxx)和recv(<- ch)接收
先考虑一个问题,如果你想让goroutine以先进先出(FIFO)的方式进入一个结构体中,你会怎么操作?
加锁!对的!channel就是用了一个锁。hchan本身包含一个互斥锁mutex
channel中队列是如何实现的
channel中有个缓存buf,是用来缓存数据的(假如实例化了带缓存的channel的话)队列。我们先来看看是如何实现“队列”的。
还是刚才创建的那个channel
ch := make(chan int, 3)
当使用send (ch <- xx)
或者recv ( <-ch)
的时候,首先要锁住hchan
这个结构体。
然后开始send (ch <- xx)
数据。
一
ch <- 1
二
ch <- 1
三
ch <- 1
这时候满了,队列塞不进去了
动态图表示为:
然后是取recv ( <-ch)
的过程,是个逆向的操作,也是需要加锁。
然后开始recv (<-ch)
数据。
一
<-ch
二
<-ch
三
<-ch
图为:
注意以上两幅图中buf
和recvx
以及sendx
的变化,recvx
和sendx
是根据循环链表buf
的变动而改变的。
至于为什么channel会使用循环链表作为缓存结构,我个人认为是在缓存列表在动态的send
和recv
过程中,定位当前send
或者recvx
的位置、选择send
的和recvx
的位置比较方便吧,只要顺着链表顺序一直旋转操作就好。
缓存中按链表顺序存放,取数据的时候按链表顺序读取,符合FIFO的原则。
send/recv的细化操作
注意:缓存链表中以上每一步的操作,都是需要加锁操作的!
每一步的操作的细节可以细化为:
- 第一,加锁
- 第二,把数据从goroutine中copy到“队列”中(或者从队列中copy到goroutine中)。
- 第三,释放锁
每一步的操作总结为动态图为:(发送过程)
或者为:(接收过程)
所以不难看出,Go中那句经典的话:Do not communicate by sharing memory; instead, share memory by communicating.
的具体实现就是利用channel把数据从一端copy到了另一端!
还真是符合channel
的英文含义:
当channel缓存满了之后会发生什么?这其中的原理是怎样的?
使用的时候,我们都知道,当channel缓存满了,或者没有缓存的时候,我们继续send(ch <- xxx)或者recv(<- ch)会阻塞当前goroutine,但是,是如何实现的呢?
我们知道,Go的goroutine是用户态的线程(user-space threads
),用户态的线程是需要自己去调度的,Go有运行时的scheduler去帮我们完成调度这件事情。关于Go的调度模型GMP模型我在此不做赘述,如果不了解,可以看我另一篇文章(Go调度原理)
goroutine的阻塞操作,实际上是调用send (ch <- xx)
或者recv ( <-ch)
的时候主动触发的,具体请看以下内容:
//goroutine1 中,记做G1
ch := make(chan int, 3)
ch <- 1
ch <- 1
ch <- 1
这个时候G1正在正常运行,当再次进行send操作(ch<-1)的时候,会主动调用Go的调度器,让G1等待,并从让出M,让其他G去使用
同时G1也会被抽象成含有G1指针和send元素的sudog
结构体保存到hchan的sendq
中等待被唤醒。
那么,G1什么时候被唤醒呢?这个时候G2隆重登场。
G2执行了recv操作p := <-ch
,于是会发生以下的操作:
G2从缓存队列中取出数据,channel会将等待队列中的G1推出,将G1当时send的数据推到缓存中,然后调用Go的scheduler,唤醒G1,并把G1放到可运行的Goroutine队列中。
假如是先进行执行recv操作的G2会怎么样?
你可能会顺着以上的思路反推。首先:
这个时候G2会主动调用Go的调度器,让G2等待,并从让出M,让其他G去使用。
G2还会被抽象成含有G2指针和recv空元素的sudog
结构体保存到hchan的recvq
中等待被唤醒
此时恰好有个goroutine G1开始向channel中推送数据 ch <- 1
。
此时,非常有意思的事情发生了:
G1并没有锁住channel,然后将数据放到缓存中,而是直接把数据从G1直接copy到了G2的栈中。
这种方式非常的赞!在唤醒过程中,G2无需再获得channel的锁,然后从缓存中取数据。减少了内存的copy,提高了效率。
之后的事情显而易见:
更多精彩内容,请关注我的微信公众号 互联网技术窝
或者加微信共同探讨交流:
参考文献:
图解Go的channel底层原理的更多相关文章
- JS原型链与instanceof底层原理
一.问题: instanceof 可以判断一个引用是否属于某构造函数: 另外,还可以在继承关系中用来判断一个实例是否属于它的父类型. 老师说:instanceof的判断逻辑是: 从当前引用的proto ...
- Java面试底层原理
面试发现经常有些重复的面试问题,自己也应该学会记录下来,最好自己能做成笔记,在下一次面的时候说得有条不紊,深入具体,面试官想必也很开心.以下是我个人总结,请参考: HashSet底层原理:(问了大几率 ...
- Neo4j图数据库简介和底层原理
现实中很多数据都是用图来表达的,比如社交网络中人与人的关系.地图数据.或是基因信息等等.RDBMS并不适合表达这类数据,而且由于海量数据的存在,让其显得捉襟见肘.NoSQL数据库的兴起,很好地解决了海 ...
- 【T-SQL进阶】02.理解SQL查询的底层原理
本系列[T-SQL]主要是针对T-SQL的总结. [T-SQL基础]01.单表查询-几道sql查询题 [T-SQL基础]02.联接查询 [T-SQL基础]03.子查询 [T-SQL基础]04.表表达式 ...
- spring框架的IOC的底层原理
1.IOC概念:spring容器创建对象并管理 2.IOC的底层原理的具体实现: 1)所使用的技术: (1). dom4j解析xml配置文件 (2).工厂设计模式(解耦合) (3).反射 第一步:配置 ...
- 深入研究Sphinx的底层原理和高级使用
深入研究Sphinx的底层原理和高级使用
- 深入研究Node.js的底层原理和高级使用
深入研究Node.js的底层原理和高级使用
- HashMap的底层原理
简单说: 底层原理就是采用数组加链表: 两张图片很清晰地表明存储结构: 既然是线性数组,为什么能随机存取?这里HashMap用了一个小算法,大致是这样实现: // 存储时: int hash = ke ...
- 操作系统底层原理与Python中socket解读
目录 操作系统底层原理 网络通信原理 网络基础架构 局域网与交换机/网络常见术语 OSI七层协议 TCP/IP五层模型讲解 Python中Socket模块解读 TCP协议和UDP协议 操作系统底层原理 ...
随机推荐
- HttpClient 302重定向
CloseableHttpClient是线程安全的,单个实例可用于处理多个HTTP请求,Http Client会自动处理所有的重定向,关闭自动重定向需要设定disableAutomaticRetrie ...
- C语言程序实现,统计字符串里面各个字符的个数在总字符个数中的比例,并打印输出。
#include<stdio.h> int main() { char *ppp= "aaassadddeeds"; ] = {};//存放字符 uint32 ccnt ...
- java中四种修饰符(private、default、protected、public)的访问权限
权限如下: no. 范围 private default protected public 1 同一包下的同一个类 √ √ √ √ 2 同一包下的不同类 × √ √ √ 3 不同包下的子类 × × √ ...
- vim 批量替换使用说明
基本语法: :[addr]s/源字符串/目的字符串/[option] 全局替换命令: :%s/源字符串/目的字符串/g [addr] 表示检索范围,省略时表示当前行. "1,20" ...
- 2018-2019-2 网络对抗技术 20165304 Exp2 后门原理与实践
后门的基本概念及实验内容 常用后门工具 netcat Win获得Linux Shell Linux获得Win Shell Meterpreter 实验内容 任务一:使用netcat获取主机操作Shel ...
- UICollectionView didSelectItemAtIndexPath实现方法
didSelectItemAtIndexPath是通过UIResponder的四个touch方法实现的(touchBegan, touchMove, touchEnd, touchCancel),因此 ...
- supervisord的安装使用
由于生产环境使用的的tomcat,项目比较重要,所以要做进程守护,本来打算自己写脚本,但是效果不理想,想了下还是用supervisord了 由于很久不用,所以写下来部署步骤 第一:安装,安装的方法有y ...
- C# webapi简单学习
创建WebApi项目: 在VS工具中创建一个ASP.NET Web应用程序 选择Webapi 一个webapi项目就创建好了 这里简单的写一个post和get两种请求的方法,由于post请求参数需要参 ...
- WMS二开:外挂页面开发培训
springboot:MAVEN结构前后台都是MVC架构基于模板引擎thymeleafapplication.yml文件里面配置了一个DEV\TEST\PROD,用于自动选择配置文件applicati ...
- pandas,读取或存储DataFrames的数据到mysql中
dataFrames格式的数据是表格形式的,mysql数据库中的数据也是表格形式的,二者可以很方便的读取存储 安装依赖的包 pip install pandas pip install sqlal ...