go基础
目录
1.golang基础
-
数组和切片
- 都是非线程安全
- slice 扩容,1.18前cap<1024,扩大2倍,后面扩大1.25倍,1.18后<256扩大2倍,而后(oldcap+3*256)/4。
- slice底层是数组,slice 是对数组的封装,数组需要使用字面量声明
- 数组是定长的,slice是可以append动态扩容,数组就是一片连续的内存,不能使用append, slice 实际上是一个结构体
type slice struct { array unsafe.Pointer // 元素指针 len int // 长度 cap int // 容量 }
- 字符串和切片,字符串s[i]不可写,0拷贝成byte后,也不可写,底层指针没变。
type StringHeader struct { Data uintptr Len int } type SliceHeader struct { Data uintptr Len int Cap int } func s2b(s string) []byte { return *(*[]byte)(unsafe.Pointer(&s)) } func b2s(b []byte) string{ return *(*string)(unsafe.Pointer(&b)) }
-
channel
- 有缓冲通道
- 发送操作会将元素放入缓冲区,只有当缓冲区满时发送操作才会阻塞。
- 接收操作会从缓冲区中取出元素,只有当缓冲区为空时接收操作才会阻塞。
- 当缓冲区不满时,发送操作和接收操作都是非阻塞的,它们之间的元素传递是异步的。
- 当关闭通道时,不能在向里面写数据panic,读数据读不到。
- 无缓冲通道
- 无缓冲通道的容量为 0,意味着发送操作和接收操作是同步的。
- 发送操作和接收操作都是原子的,即发送操作和接收操作之间的元素传递是保证原子性的。
- 当关闭通道时,不能在向里面写数据panic,还有数据的话,可以读数据。
- 有缓冲通道
-
闭包函数
- 闭包函数里引用的外部变量,是在堆还是栈内存申请的,取决于,你这个闭包函数在函数 Return 后是否还会在其他地方使用,若会, 就会在堆上申请,若不会,就在栈上申请。
- 闭包函数里,引用的外部变量,存储的并不是对值的拷贝,存的是值的指针。defer使用变量快照失效。
- 函数的返回值里若写了变量名,则该变量是在上级的栈内存里申请的,return 的值,会直接赋值给该变量。
-
sync.Map、sync.Mutex、sync.WaitGroup、sync.Cond
-
go协程和线程
- 内存占用,go协程内存占用小,会自动扩容
- 创建销毁,go协程runtime管理,线程是内核级别开销大
- 线程切换开销大
2.GMP
什么是GMP?
- G:Goroutine,也就是 go 里的协程,是用户态的轻量级线程,具体可以创建多个 goroutine ,取决你的内存有多大,一个 goroutine 大概需要 4k 内存,只要你不是在 32 位的机器上,那么创建个几百万个的 goroutine 应该没有问题。
- M:Thread,也就是操作系统线程,go runtime 最多允许创建 10000 个操作系统线程,超过了就会抛出异常
- P:Processor,处理器,数量默认等于开机器的cpu核心数,若想调小,可以通过 GOMAXPROCS 这个环境变量设置。
- M:N模型: runtime启动时会创建M个线程,之后N个协程依附在M上执行。
GMP工作原理
- 初始化:当程序启动时,Go运行时会初始化一定数量的M和P,并开始执行程序的主函数。
- 创建goroutines:当程序中有新的goroutine被创建时,它们会被加入到某个P的本地队列(localrun ueue)中。如果本地队列已满,goroutines会被放到全局队列(globalrunqueue)中。
- 调度执行:每个P都有一个调度器(scheduler),它会负责从本地队列或全局队列中选择goroutines来执行。调度器会根据一定的策略选择goroutines,并将它们分配给关联的M来执行。
- 执行goroutines:M会执行与其关联的goroutines。当一个goroutine阻塞时(如等待I/O操作完成),与之关联的M会释放处理器,去执行其他goroutines,以充分利用系统资源。
- 退出和回收:当goroutine执行完成或被取消时,它会退出,并释放相关的资源。当程序退出时,所有的M和P也会被销毁,释放系统资源。
go调度时机
- goroutine:go 创建一个新的 goroutine,Go scheduler 会考虑调度
- GC:由于进行 GC 的 goroutine 也需要在 M 上运行,因此肯定会发生调度。当然,Go scheduler 还会做很多其他的调度,例如调度不涉及堆访问的 goroutine 来运行。GC 不管栈上的内存,只会回收堆上的内存
- 系统调用SysCall,cgo调用:当 goroutine 进行系统调用时,会阻塞 M,所以它会被调度走,同时一个新的 goroutine 会被调度上来
- 同步访问,阻塞:atomic,mutex,channel 操作等会使 goroutine 阻塞,因此会被调度走。等条件满足后(例如其他 goroutine 解锁了)还会被调度上来继续运行
什么情况阻塞
- 系统调用和cgo调用
- 通道操作,锁,网络IO,时间等待
Go调度器
将P绑定到一个合适的M 为P选中一个G来执行
GO调度策略
- 复用线程
- 窃取机制,从其他M绑定的P偷G,不让P空闲
- 交接,G阻塞时,M将P解绑,把P转移给其他空闲的M执行
- GOMAXPROCS
- 设置P的数量
- 抢占调度
- 基于信号抢占:当一个goroutine处于运行状态时,超过20ms,直接让出cpu运行权
- 协作式抢占:sysmon监控线程发现有G阻塞时,打上标记,让出CPU,切换到主协程里。
GMP为什么要有P
- M需要从全局队列里获取G,高并发下,锁的性能瓶颈,P有自己的本地队列,减少锁的竞争。
- 当M中的一个G阻塞时,P会重新选择或创建一个M执行G,提高效率。
- GMP模型中复用线程,如果本地P空闲会从其他M绑定的P窃取G,提高了资源利用率。
3.内存管理
对象大小
类别 | 大小 |
---|---|
微对象 | (0, 16B) |
小对象 | [16B, 32KB] |
大对象 | (32KB, +∞) |
内存管理组件
多级缓存,Go 语言的内存分配器包含内存管理单元、线程缓存、中心缓存和页堆几个重要组件,几种最重要组件对应的数据结构 runtime.mspan、runtime.mcache、runtime.mcentral 和 runtime.mheap. mheap,mcentral加锁,mcache独立的p拥有,不需要加锁。
- mheap:全局的内存起源,访问要加全局锁
- mcentral:每种对象大小规格(全局共划分为 68 种)对应的缓存,锁的粒度也仅限于同一种规格以内
- mcache:每个 P(正是 GMP 中的 P)持有一份的内存缓存,访问时无锁
mspan内存管理单元
(1)page:最小的存储单元.
Golang 借鉴操作系统分页管理的思想,每个最小的存储单元也称之为页 page,但大小为 8 KB
(2)mspan:最小的管理单元.
mspan 大小为 page 的整数倍,且从 8B 到 80 KB 被划分为 67 种不同的规格,分配对象时,会根据大小映射到不同规格的 mspan,从中获取空间.
type mspan struct {
// 标识前后节点的指针
next *mspan
prev *mspan
// ...
// 起始地址
startAddr uintptr
// 包含几页,页是连续的
npages uintptr
// 标识此前的位置都已被占用
freeindex uintptr
// 最多可以存放多少个 object
nelems uintptr // number of object in the span.
// bitmap 每个 bit 对应一个 object 块,标识该块是否已被占用
allocCache uint64
// ...
// 标识 mspan 等级,包含 class 和 noscan 两部分信息,不同等级分配不同大小内存
spanclass spanClass
// ...
}
• mspan 是 Golang 内存管理的最小单元
• mspan 大小是 page 的整数倍(Go 中的 page 大小为 8KB),且内部的页是连续的(至少在虚拟内存的视角中是这样)
• 每个 mspan 根据空间大小以及面向分配对象的大小,会被划分为不同的等级
• 同等级的 mspan 会从属同一个 mcentral,最终会被组织成链表,因此带有前后指针(prev、next)
• 由于同等级的 mspan 内聚于同一个 mcentral,所以会基于同一把互斥锁管理
• mspan 会基于 bitMap 辅助快速找到空闲内存块(块大小为对应等级下的 object 大小),此时需要使用到 Ctz64 算法.
内存分配
- 微对象 (0, 16B) — 先使用微型分配器,再依次尝试线程缓存、中心缓存和堆分配内存;
- 小对象 [16B, 32KB] — 依次尝试使用线程缓存、中心缓存和堆分配内存;
- 大对象 (32KB, +∞) — 直接在堆上分配内存;如果都没有,直接向操作系统分配内存
GO GC
GC原理
主流GC算法
- 引用计数
- 为每个对象维护一个计数,当引用对象销毁时计数-1,为0的时候回收
- 优点:对象回收快
- 缺点:不好处理循环引用
- 分代收集
- 按照对象生命周期长短划分不同空间
- 优点:性能好
- 缺点:算法复杂
- 标记清除
- 从根变量开始遍历所有引用对象,没有被标记的清楚
- 优点:解决引用计数问题
- 缺点:需要STW,耗时慢
GO-GC标记清除
- 标记阶段(Marking Phase):
- 初始标记(Initial Mark):GC会暂停整个程序(STW,Stop The World),标记从根对象(根对象包括全局变量、栈上的局部变量等)可以直接访问到的所有对象。这部分通常会快速完成。
- 并发标记(Concurrent Mark):在初始标记之后,GC和程序同时运行。GC会继续追踪并标记在初始标记之后新创建或被修改的对象。
- 重新标记(Re-mark):在并发标记阶段结束时,GC会再次暂停程序,确保没有遗漏任何存活对象。这是为了确保并发标记阶段新创建或被修改的对象也能被正确标记。
- 清除阶段(Sweeping Phase):
- 并发清除(Concurrent Sweep):在标记阶段标记完所有存活对象之后,GC会并发清理所有未被标记的对象,回收它们所占用的内存。这个阶段通常也是并发进行的,不会对程序的运行造成明显的暂停。
标记算法-三色标记法
在标记过程中,GC使用三种颜色(白色、灰色和黑色)来跟踪对象状态。
- 白色表示未标记对象,
- 灰色表示已标记但其子对象尚未完全标记的对象,
- 黑色表示已标记且其子对象也已完全标记的对象。
遍历过程:
- 1.创建白色、灰色、黑色集合
- 2.将所有对象放入白色集合中
- 3.遍历所有root对象,将遍历的到对象从白色集合转移到灰色集合中。
- 4.遍历灰色对象,将灰色对象引用的对象从白色集合放到黑色集合,自身标记成黑色
- 5.重复步骤4,知道灰色中无任何对象,其中用到的2个机制:
- 写屏障:让程序和GC并发运行,减少STW次数,在每次对象被引用时记录变化,减少重新标记时工作量
- 辅助GC:防止内存分配过快。
- 6.回收白色对象
什么时候触发STW
- 初始标记(Initial Mark):
- 时机:在标记阶段的开始,GC需要暂停所有goroutine,以标记从根对象(例如全局变量、栈上的局部变量等)可以直接访问到的所有对象。
- 目的:确保从根对象开始的标记过程的准确性。这一阶段通常会非常短暂,因为只是标记直接可达的对象。
- 重新标记(Re-mark):
- 时机:在并发标记阶段结束之后,GC会再次触发STW暂停,以确保在并发标记阶段期间新创建或被修改的对象也能被正确标记。
- 目的:确保所有存活对象都被正确标记,包括在并发标记阶段期间发生变动的对象。这是为了保证最终标记结果的完整性和准确性。
- 抢占式GC暂停:
- 时机:在GC运行过程中,如果发现某些goroutine长时间占用资源,导致GC无法及时完成必要的操作,GC可能会强制暂停这些goroutine。
- 目的:确保垃圾回收过程能够顺利进行,避免因为某些长时间运行的goroutine导致回收过程被延迟。
- 调节内存分配压力:
- 时机:在内存分配压力较大时,GC可能会增加STW暂停的频率,以便更频繁地进行垃圾回收,防止内存耗尽。
- 目的:保证程序在高内存压力下仍然能够正常运行,避免因为内存不足导致程序崩溃。
GC调优
- 控制内存分配速度,限制goroutine数量
- 少量使用+连接string
- slice提前申请内存,防止频繁扩容
- 避免map key对象过多,增加扫描时间
- 变量复用,避免重复分配
- 降低GC频率