go语言底层原理简析

  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

T.Mv *T.Mp
func Mv(tv T, a int) int
func Mp(tp *T, f float32) float32
方法
“继承”
在Go中没有继承,将一个带方法的类型匿名嵌入到另一个结构体中 ,则这个结构体也会拥有嵌入的类型的方法。
Daemon中没有定义 ServeHTTP方法,但“继承” 了匿名成员HttpHandler的该 方法。
go语言底层原理浅析
主讲人:葛午未 组员:xxx
数据结构
1
数据类型
值类型:
byte、int、int32、float32、float64、string、数组 …
引用类型:
slice、map、channel
注:引用类型可以简单的理解为指针类型,它们都是通过make完成 初始化
数组与切片
数组结构示意:
func (h *HttpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
…… }
方法
“多态”
Go本身不具有多态的特性,但是,使用interface可以编写具有多态 功能的类绑定的方法。
type Draw interface{ Paint()
栈管理
连续栈:不再把栈分成一段一段的。
开始栈只有一个段,发生函数调用时检查栈是否够用,需要更 多的栈空间时,直接new一个2倍大的栈空间,并将原先栈空 间中的数据拷贝到新的栈空间中。
栈的收缩是垃圾回收的过程中实现的.当检测到栈只使用了不 到1/4时,栈缩小为原来的1/2。
栈管理
栈溢出检查:
函数调用时比较SP是否大于当前 协程的stackguard,如果是,则 会调用runtime.morestack函数。
interface
例子:
内存结构:
函数调用
6
多值返回
C多值返回函数调用方式:
go多值返回函数调用方式:
多值返回
相较于传统C中的callee-save模式,go编译器采用的是caller-save模 式,即,由调用者负责保存寄存器。
被调函数将运行结果写入栈中 的返回结果位,而不像c中将结 果pop到eax寄存器。
调度时机:
goroutine调度发生在函数调用时,如果goroutine已超时运行 ,则在函数调用时将stackguard改为StackPreempt,触发 morestack。
morestack检查stackguard被改为了StackPreempt,触发 runtime.Gosched进行调度。
闭包
逃逸分析:
本应在栈上分配 的内存,放在堆 上分配了
闭包
闭包是匿名函数与匿名函数所引用环境的组合。匿名函数有动态创 建的特性,该特性使得匿名函数不用通过参数传递的方式,就可以 直接引用外部的变量。
闭包结构:
闭包
例子:
运行结果:
并发情况下,需处理好循环中的闭包引用的外部变量。
方法
对象的方法调用相当于普通函数调用的一个语法糖衣。
异步(带缓冲区)写:
如果缓冲区不满,数据放到channel缓冲区中,调用者返回。
如果缓冲区满了,将当前goroutine和数据一起作为SudoG结构 体挂在sendq队列中,表示因写channel而阻塞。
channel
同步/异步读:
同步/异步读与同步/异步写过程类似,不再赘述。
空通道与关闭通道:
读或者写一个nil的channel的操作会永远阻塞。 读一个关闭的channel会立刻返回一个channel元素类型的零值 。 写一个关闭的channel会导致panic。
栈管理
分段栈:实现了一种不连续但是可以持续增长的栈。
开始栈只有一个段,发生函数调用时检查栈是否够用,需要更 多的栈空间时,会分配一个新的段,和上一个段双向链接。 当新分配的段使用完毕后,新段会被释放掉。
问题:例如,for循环执行一个比较耗空间的函数,导致函数执行时进
行段分配,返回时,进行段销毁。多次栈的扩容和收缩,造成 很大的性能损失(Stack Split)。
G-P-M模型
G结构: G是goroutine的缩写,相当于操作系统中的进程控制块。
struct Gobuf { uintptr sp; byte* pc; ...
};
G-P-M模型
M结构:
M是machine 的缩写,是对 机器的抽象, 每个m都是对 应到一条操作 系统的物理线 程。
用于调度的 G(sysmon),采 用当前M的栈空 间
G-P-M模型
工作流窃取
在P队列上的goroutine全部调度完了之后,对应的M首先会尝试从 global runqueue中获取goroutine进行调度。如果golbal runqueue中没有 goroutine,当前M会从别的M对应P的local runqueue中抢一半的goroutine放入 自己的P中进行调度。
假如每个goroutine分配固定栈大小并且不能增长,太小则会导 致溢出,太大又会浪费空间,无法存在许多的goroutine
解决办法:
goroutine可以初始时只给栈分配很小的空间,然后随着使用过 程中的需要自动地增长,不需要那么大的空间时,栈空间也要 自动缩小。go 1.3之前采用分段栈,之后改为连续栈。
注:没有函数调用的死循环不会被调度!
channel
结构示意: Go 语言的并发模型参考了 CSP 理论,其中执行实体对应的是
goroutine, 消息通道对应的就是 channel。
channel
同步(不带缓冲区)写:
检查Hchan结构体的recvq链表是否为空,即是否有读该管道 而阻塞的goroutine。如果有则正常写channel,否则阻塞。
运行结果:
WARNING: DATA RACE Read at 0x00c420094008 by goroutine 7: main.main.func1()
/Users/zach/workspace/go/src/test/main.go:28 +0x59
内存管理
6
内存划分
初始化时,go申请一段连续地址,并切分分为三块:spans bitmap areana。
具体类型结构: 空接口结构:
该类型实现 的所有方法
interface
带方法的interface底层使用的数据结构与空interface不同,它是实现 运行时多态的基础。
例如: type I interface { String()
}
虚表类型结构: 带方法的接口结构:
接口声明的 方法列表
类型定义的 方法列表
M:os线程(即操作系统内核提供的线 程)。
G:goroutine,其包含了调度一个协程 所需要的堆栈以及instruction pointer(IP 指令指针),以及其他一些重要的调度 信息。
P:M与P的中介,实现m:n 调度模型的 关键,M必须拿到P才能对G进行调度, P其实限定了golang调度其的最大并发度 。
如果recvq不为空,将一个SudoG结构体出队列,将传给通道 的数据拷贝到SudoG结构体中的elem域,并将SudoG中的g放 到就绪队列中,状态置为Grunnable。
如果recvq为空,否则要将当前goroutine阻塞。此时将一个 SudoG结构体,挂到通道的sendq链表中。当前goroutine会被 设置为Gwait状态。
并发写:
运行结果:
fatal error: concurrent map writes
map
注意事项:golang内建map不是并发安全的;
解决方案:
interface
interface可以被当作“duck”类型使用。go是类型安全的,类型之间不 能相互转换,类型可以与interface进行转换。
64位系统中,arena区域就是heap,是供分配维护的内存池, 对应区域大小是512G。
bitmap区域是标识arena中那些地址保存了对象,及对象中是 否包含了指针。
span是页管理单元,是内存分配的基本单位,其中一个指针对 应arena中1个虚拟地址页大小。
内存池
MHeap: 分配堆,按页 的粒度进行管理 (4kB); MSpan: 一些由 MHeap管理的页; MCentral: 对于给定 尺寸类别的共享的 free list理的页; MCache: 用于小对象 的每M一个的cache 。
当前M使用的内 存缓存
G-P-M模型
P结构:
P是Processor的缩写,当M执行Go代码时,它需要关联一个P 。P代表了实际的并发度。
P的局部队列, 优先从该队源自文库取 待执行的G
生命周期
栈管理
优势:
Go支持几十万协程并发,其中一个重要原因是协程的栈空间 很小(1.2之前为4k,之后为8k)。
存在的问题:
TLS对应G结构 体的stackguard
抢占式调度
策略:
goroutine没有时间片、优先级等复杂的设置。
运行时库周期执行g0,g0执行sysmon函数,sysmon函数检查 goroutine是否运行了很长时间。
如果一个goroutine运行了很长时间(超过10ms),则在合适 的时机对其进行调度。
切片结构示意:
数组与切片
数据结构:
切片扩容:
如果新的大小是当前大小2倍以上,则大小增长为新大小; 否则循环以下操作:
如果当前大小小于1024,按每次2倍增长; 否则每次按当前大小1/4增长;
数组与切片
例子:
结果:
数组与切片
分析:
map
结构示意:
查找过程:
按key的类型采用相应的hash算法得到key的hash值; 将hash值的低位当作Hmap结构体中buckets数组的index; 将hash的高8位存储在了bucket的tophash中,高8位作为主键顺序匹 配tophash中的值,找到对应key,继而找到value;
G-P-M模型
系统调用
调用system call陷入内核没有返回之前,为保证调度的并发性,golang 调度器 在进入系统调用之前从线程池拿一个线程或者新建一个线程,当前P交给新的线 程M1执行。
G0返回之后,需要找一个可用的P继续运行,如果没有则将其放在全局队列等 待调度。M0待G0返回后退出或放回线程池。
}
func main(){ var draw Draw draw = &Circular{"画一个圆形"} draw.Paint() draw = &Triangular{"画一个三角形"} draw.Paint()
}
go routine
6
G-P-M模型
为什么引入协程?
核心原因为goroutine的轻量级,无论是从进程到线程,还是从线程到协程,其 核心都是为了使得我们的调度单元更加轻量级。可以轻易得创建几万几十万的 goroutine而不用担心内存耗尽等问题。
扩容时机(扩容填充因子):如果grow的太频繁,会造成空间的利 用率很低, 如果很久才grow,会形成很多的overflow buckets,查 找的效率也会下降。
#define LOAD 6.5:如果table中元素的个数大于table中能容纳的元 素个数的65%,则触发扩容。
map
例子: 结构定义:
}
type Circular struct{ Name string
}
type Triangular struct{ Name string
}
func (c *Circular)Paint(){ fmt.Println("c:",c.Name)
}
func (c * Triangular)Paint(){ fmt.Println("c:",c.Name)
map
map数据结构:
bucket数据结构 :
map
增量扩容:
哈希表大小始终为2的指数倍,每次扩容都变为原来大小的两倍。如 ,扩容前的哈希表大小为2^B,扩容之后的大小为2^(B+1);
扩容后,需将旧的pair重新哈希到新的table上,该过程并非一次到位 ,而是逐步完成,如,insert或remove时每次搬移1-2个pair;只有 所有的bucket都从旧表移到新表之后,才会将oldbucket释放掉。
channel
例子:
func main() { var wg sync.WaitGroup var count int var ch = make(chan bool, 10) for i := 0; i < 10; i++ { wg.Add(1) go func() {
ch <- true count++ time.Sleep(time.Millisecond) count-<-ch wg.Done() }() } wg.Wait() }
相关文档
最新文档