go-advanced-learning-week03
✨你好啊,我是“ 罗师傅”,是一名程序猿哦。
🌍主页链接:楚门的世界 - 一个热爱学习和运动的程序猿
☀️博文主更方向为:分享自己的快乐
❤️一个“不想让我曾没有做好的也成为你的遗憾”的博主。
💪很高兴与你相遇,一起加油!
前言
随着Go语言基本语法学习结束,迫切需要对Go进行更深入的学习。
主要分为三个模块:线程加锁、线程调度、内存管理
线程加锁
锁
- Go语言不仅仅提供基于CSP的通讯模型,也支持基于共享内存的多线程数据访问
- Sync包提供了锁的基本原语
- sync.Mutex 互斥锁
- Lock()加锁,Unlock()解锁
- sync.RWMutex 读写分离锁
- 不限制并发读,只限制并发写和并发读写
- sync.waitGroup
- 等待一组goroutine返回
- sync.Once
- 保证某段代码只执行一次
- sync.Cond
- 让一组 goroutine 在满足特定条件时被唤醒
线程调度
深入理解Go语言线程调度
- 进程:资源分配的基本单位
- 线程:调度的基本单位
- 无论是线程还是进程,在linux中都以task_struct描述,从内核角度看,与进程无本质区别
- Glibc中的pthread库提供NPTL(Native POSIX Threading Library)支持
通过图片可以看出:进程是由一个
PID=1
不断fork出来的,线程在同一进程中的资源是共享的
Linux进程的内存使用
CPU对内存
- CPU 上有个Memory Management Unit(MMU) 单元
- CPU 把虚拟地址给MMU,MMU 去物理内存中查询页表,得到实际的物理地址
- CPU 维护一份缓存Translation Lookaside Buffer(TLB) 缓存虚拟地址和物理地址的映射关系
进程切换开销
- 直接开销
- 切换页表全局目录(PGD)
- 切换内核态堆栈
- 切换硬件上下文(进程恢复前,必须装入寄存器的数据统称为硬件上下文)
- 刷新TLB
- 系统调度器的代码执行
- 间接开销
- CPU 缓存失效导致的进程需要到内存直接访问的IO操作变多
线程切花开销
- 线程本质上只是一批共享资源的进程,线程切换本质上依然需要内核进行进程切换
- 一组线程因为共享内存资源,因此一个进程的所有线程共享虚拟地址空间,线程切换相比进程切换,主要节省了虚拟地址空间的切换。
用户线程
无需内核帮助,应用程序在用户空间创建的可执行单元,创建销毁完全在用户态完成
Goroutine
Go 语言基于
GMP(=MPG)
模型实现用户态线程
- G:表示goroutine, 每个goroutine 都有自己的栈空间,定时器,初始化的栈空间2k左右,空间会随着需求增长。
- M:抽象化代表内核线程,记录内核线程栈信息,当goroutine调度到线程时,使用该goroutine自己的栈信息(相当于把CPU分给了当前的goroutine了)
- P:代表调度器,负责调度goroutine,维护一个本地goroutine队列,M从P上获得goroutine并执行,同时还负责部分内存的管理。
GMP 模型细节
G 所处的位置
- 进程都有一个全局的G队列
- 每一个P拥有自己的本地执行队列
- 有不在运行队列中的 G
- 处于channel阻塞态的G被放在sudog
- 脱离P绑定在M上的G,如系统调用
- 为了复用,执行结束进入P的gFree列表中的G(线程池)
Goroutine 创建过程
- 获取或者创建新的Goroutine结构体
- 从处理器的gFree 列表中查找空闲的Goroutine
- 如果不存在空闲的Goroutine,会通过runtime.malg 创建一个栈大小足够的新结构体
- 将函数传入的参数移到Goroutine的栈上
- 更新Goroutine 调度相关的 属性,更新状态为_Grunnable
- 返回的Goroutine 会存储到全局变量allgs中
将Goroutine放到运行队列上
- Goroutine设置到处理器的runnext作为下一个处理器执行的任务(分配CPU)
- 当处理器的本地运行队列已经没有剩余空间时,就会把本地队列中的一部分Goroutine和待加入的Goroutine通过runtime.runqputslow添加到调度器持有的全局运行队列上。
调度器行为
- 为了保证公平,当全局运行队列中有待执行的Goroutine时,通过schedtick保证有一定几率会从全局的运行队列中查找对应的Goroutine
- 从处理器本地的运行队列中查找待执行的Goroutine
- 如果前两种方法都没有找到Goroutine,会通过runtime.findrunnable进行阻塞地查找Goroutine
- 从本地运行队列、全局运行队列中查找
- 从网络轮询器中查找是否有Goroutine等待运行
- 通过runtime.runqsteal 尝试从其他随机的处理器中窃取待运行的Goroutine
内存管理
关于内存管理的争论
堆内存管理
- 初始化连续内存块作为堆
- 有内存申请的时候,Allocator从堆内存的未分配区域分割小内存块
- 用链表将已分配内存连接起来
- 需要信息描述每个内存块的元数据:大小,是否使用,下一个内存块的地址等
TCMalloc 概览
- page:内存页,一块8K大小的内存空间。Go与操作系统之间的内存申请和释放,都是以page为单位的
- span:内存块,一个或多个连续的page组成一个span
- sizeclass:空间规格,每个span都带有一个sizeclass,标记着该span中的page应该如何使用
- object:对象,用来存储一个变量数据内存空间,一个span在初始化时,会被切割成一堆等大的object;假设object的大小是16B,span大小是8K,那么就会把span中的page就会被初始化8K/16B = 512个object。所谓内存分配,就是分配一个object出去。
Go语言内存分配
- mcache:小对象的内存分配直接走
- size class 从1到66,每个class两个span
- Span 大小是8KB,按span class 大小切分
- mcentral
- Span内的所有内存块都被占用时,没有剩余空间继续分配对象,mcache会向mcentral申请1个span,mcache拿到span后继续分配对象
- 当mcentral向mcache提供span时,如果没有符合条件的span,mcentral会向mheap申请span
- mheap
- 当mheap没有足够的内存时,mheap会向OS申请内存
- Mheap把Span组织成了树结构,而不是链表
- 然后把Span分配到heapArena进行管理,它包含地址映射和span是否包含指针等位图
- 为了更高效的分配、回收和再利用内存
内存回收
内存回收 不同语言使用的算法是不一样的~
- 引用计数(Python,PHP,Swift)
- 对每一个对象维护一个引用计数,当引用该对象的对象被销毁的时候,引用计数减1,当引用计数为0的时候,回收该对象(if count== 0 回收)
- 优点:对象可以很快的被回收,不会出现内存耗尽或达到某个阈值时才回收
- 缺点:不能很好的处理循环引用,而且实时维护引用计数,也有一定的代价
- 标记-清除(Golang)
- 从根变量开始遍历所有引用的对象,引用的对象标记为“被引用”,没有被标记的进行回收
- 优点:解决引用计数的缺点
- 缺点:需要STW(stop the world),即要暂停程序运行
- 分代收集(Java)
- 按照生命周期进行划分不同的代空间,生命周期长的放入老年代,短的放入新生代,新生代的回收频率高于老年代的频率
mspan
mspan
是分配内存时的基本单元(machine)
- allocBits
- 记录了每块内存分配的情况
- gcmarkBits
- 记录了每块内存的引用情况,标记阶段对每块内存进行标记,有对象引用的内存标记为1,没有标记为0
- 这两个位图的数据结构是完全一致的,标记结束则进行内存回收,回收的时候,将allocbits指向gcmarkBits, 标记过的则存在,未进行标记的则进行回收。
GC 工作流程
Golang GC的大部分处理是和用户代码并行的
- Mark:
- Mark Prepare:初始化GC任务,包括开启写屏障(write barrier)和辅助GC(mutator assist),统计root对象的任务数量等。这个过程需要STW
- GC Drains:扫描所有root对象,包括全局指针和goroutine(G)栈上的指针(扫描对应G栈时需停止该G),将其加入标记队列(灰色队列),并循环处理灰色队列的对象,知道灰色队列为空。该过程后台并行执行
- Mark Termination:完成标记工作,重新扫描(re-scan)全局指针和栈。因为Mark和用户程序并行的,所以在Mark过程中可能会有新的对象分配和指针赋值,这个时候就需要通过写屏障(write barrier)记录下来,re-scan再检查一下,这个过程也是会STW的
- Sweep:按照标记结果回收所有的白色对象,该过程后台并行执行
- Sweep Termination:对未清扫的span进行清扫,只有上一轮的GC的清扫工作完成才可以开始新一轮的GC
三色标记
- GC开始时,认为所有object都是白色,即垃圾
- 从root区开始遍历,被触达的object置成灰色
- 遍历所有灰色object,将他们内部的引用变量置成灰色,自身置成黑色
- 循环第3步,直到没有灰色object了,只剩下了黑白两种,白色都是垃圾
- 对于黑色object,如果在标记期间发生了写操作,写屏障会在真正赋值前将新对象标记为灰色
- 标记过程中,mallocgc新分配的object,会先被标记成黑色再返回
垃圾回收触发机制
- 内存分配量达到阈值触发GC
- 每次内存分配时都会检查当前内存分配量是否已达到阈值,如果达到阈值则立即启动GC。
阈值 = 上次GC内存分配量 * 内存增长率
- 内存增长率由环境变量GOGC控制,默认为100,即每当内存扩大一倍时启动GC。
- 定期触发GC
- 默认情况下,最长2分钟触发一次GC,这个间隔在src/runtime/proc.go:forcegcperiod变量中被声明
- 手动触发
- 程序代码中也可以使用**runtime.GC()**来手动触发GC。这主要用于GC性能测试和统计
❤️❤️❤️忙碌的敲代码也不要忘了浪漫鸭!
🍉🍉🍉第三周golang的进阶学习到这里就结束啦
期待下周的学习,我们下周见 拜拜咯~~💪。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 楚门的世界!
评论