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