go语言如何扩容 go语言 gc( 二 )


缓冲区原理介绍
go字节缓冲区底层以字节切片做存储 , 切片存在长度len与容量cap, 缓冲区写从长度len的位置开始写,当lencap时,会自动扩容 。缓冲区读会从内置标记off位置开始读(off始终记录读的起始位置),当off==len时,表明缓冲区已全部读完
并重置缓冲区(len=off=0),此外当将要内容长度+已写的长度(即len) = cap/2时,缓冲区前移覆盖掉已读的内容(off=0,len-=off) , 从避免缓冲区不断扩容
Go语言——goroutine并发模型参考:
Goroutine并发调度模型深度解析手撸一个协程池
Golang 的 goroutine 是如何实现的?
Golang - 调度剖析【第二部分】
OS线程初始栈为2MB 。Go语言中,每个goroutine采用动态扩容方式,初始2KB,按需增长,最大1G 。此外GC会收缩栈空间 。
BTW,增长扩容都是有代价的 , 需要copy数据到新的stack,所以初始2KB可能有些性能问题 。
更多关于stack的内容 , 可以参见大佬的文章 。聊一聊goroutine stack
用户线程的调度以及生命周期管理都是用户层面,Go语言自己实现的,不借助OS系统调用,减少系统资源消耗 。
Go语言采用两级线程模型,即用户线程与内核线程KSE(kernel scheduling entity)是M:N的 。最终goroutine还是会交给OS线程执行 , 但是需要一个中介,提供上下文 。这就是G-M-P模型
Go调度器有两个不同的运行队列:
go1.10\src\runtime\runtime2.go
Go调度器根据事件进行上下文切换 。
调度的目的就是防止M堵塞,空闲,系统进程切换 。
详见Golang - 调度剖析【第二部分】
Linux可以通过epoll实现网络调用 , 统称网络轮询器N(Net Poller) 。
文件IO操作
上面都是防止M堵塞,任务窃取是防止M空闲
每个M都有一个特殊的G,g0 。用于执行调度,gc,栈管理等任务,所以g0的栈称为调度栈 。g0的栈不会自动增长,不会被gc,来自os线程的栈 。
go1.10\src\runtime\proc.go
G没办法自己运行 , 必须通过M运行
M通过通过调度 , 执行G
从M挂载P的runq中找到G , 执行G
goland map底层原理map 是Go语言中基础的数据结构,在日常的使用中经常被用到 。但是它底层是如何实现的呢?
总体来说golang的map是hashmap,是使用数组+链表的形式实现的,使用拉链法消除hash冲突 。
golang的map由两种重要的结构,hmap和bmap(下文中都有解释),主要就是hmap中包含一个指向bmap数组的指针 , key经过hash函数之后得到一个数,这个数低位用于选择bmap(当作bmap数组指针的下表),高位用于放在bmap的[8]uint8数组中 , 用于快速试错 。然后一个bmap可以指向下一个bmap(拉链) 。
Golang中map的底层实现是一个散列表,因此实现map的过程实际上就是实现散表的过程 。在这个散列表中,主要出现的结构体有两个 , 一个叫 hmap (a header for a go map) , 一个叫 bmap (a bucket for a Go map,通常叫其bucket) 。这两种结构的样子分别如下所示:
hmap :
图中有很多字段 , 但是便于理解map的架构,你只需要关心的只有一个,就是标红的字段: buckets数组。Golang的map中用于存储的结构是bucket数组 。而bucket(即bmap)的结构是怎样的呢?
bucket :
相比于hmap , bucket的结构显得简单一些,标红的字段依然是“核心”,我们使用的map中的key和value就存储在这里 。“高位哈希值”数组记录的是当前bucket中key相关的“索引”,稍后会详细叙述 。还有一个字段是一个指向扩容后的bucket的指针,使得bucket会形成一个链表结构 。例如下图:

推荐阅读