go知识

234 阅读12分钟

GMP

概述

image.png

GMP模型

image.png 全局队列(Global Queue):存放等待运行的 G。
P 的本地队列:同全局队列类似,存放的也是等待运行的 G,存的数量有限,不超过 256 个。新建 G’时,G’优先加入到 P 的本地队列,如果队列满了,则会把本地队列中一半的 G 移动到全局队列。
P 列表:所有的 P 都在程序启动时创建,并保存在数组中,最多有 GOMAXPROCS(可配置) 个。
M列表:内核线程列表.在程序初始化时进行创建.线程想运行任务就得获取 P,从 P 的本地队列获取 G,P 队列为空时,M 也会尝试从全局队列拿一批 G 放到 P 的本地队列,或从其他 P 的本地队列偷一半放到自己 P 的本地队列。M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去

go func () 调度流程

image.png 从上图我们可以分析出几个结论:

​ 1、我们通过 go func () 来创建一个 goroutine;

​ 2、有两个存储 G 的队列,一个是局部调度器 P 的本地队列、一个是全局 G 队列。新创建的 G 会先保存在 P 的本地队列中,如果 P 的本地队列已经满了就会保存在全局的队列中;

​ 3、G 只能运行在 M 中,一个 M 必须持有一个 P,M 与 P 是 1:1 的关系。M 会从 P 的本地队列弹出一个可执行状态的 G 来执行,如果 P 的本地队列为空,就会想其他的 MP 组合偷取一个可执行的 G 来执行;

​ 4、一个 M 调度 G 执行的过程是一个循环机制;

​ 5、当 M 执行某一个 G 时候如果发生了 syscall 或则其余阻塞操作,M 会阻塞,如果当前有一些 G 在执行,runtime 会把这个线程 M 从 P 中摘除 (detach),然后再创建一个新的操作系统的线程 (如果有空闲的线程可用就复用空闲线程) 来服务于这个 P;

​ 6、当 M 系统调用结束时候,这个 G 会尝试获取一个空闲的 P 执行,并放入到这个 P 的本地队列。如果获取不到 P,那么这个线程 M 变成休眠状态, 加入到空闲线程中,然后这个 G 会被放入全局队列中。

调度器的生命周期

image.png

M0

M0 是启动程序后的编号为 0 的主线程,这个 M 对应的实例会在全局变量 runtime.m0 中,不需要在 heap 上分配,M0 负责执行初始化操作和启动第一个 G, 在之后 M0 就和其他的 M 一样了。

G0

G0 是每次启动一个 M 都会第一个创建的 goroutine,G0 仅用于负责调度的 G,G0 不指向任何可执行的函数,每个 M 都会有一个自己的 G0。在调度或系统调用时会使用 G0 的栈空间,全局变量的 G0 是 M0 的 G0。
我们来跟踪一段代码

package main 
import "fmt" 
func main() {
    fmt.Println("Hello world") 
}

接下来我们来针对上面的代码对调度器里面的结构做一个分析。

也会经历如上图所示的过程:

协程调度的生命周期

  1. runtime 创建最初的线程 m0 和 goroutine g0,并把 2 者关联。
  2. 调度器初始化:初始化 m0、栈、垃圾回收,以及创建和初始化由 GOMAXPROCS设定P 列表。
  3. 调用runtime.newproc()函数,创建主goroutine,newproc会从当前p中获取一个G结构体,如果没有就创建个.在切换到G0栈上进行当前G栈的分配并将PC指向runtime.exit的地址,指定的入口函数是runtime.main()函数,这是程序启动后第1个真正的goroutine, 调用runtime.mstart()函数,当前线程进入调度循环。一般情况下线程调用mstart()函数进入调度循环后不会再返回。进入调度循环的线程会去执行上一步创建的goroutine
  4. 调用runtime.mstart()函数,当前线程进入调度循环。一般情况下线程调用mstart()函数进入调度循环后不会再返回。进入调度循环的线程会去执行上一步创建的goroutine
  5. 主goroutine得到执行后,runtime.main()函数会设置最大栈大小、启动监控线程sysmon、初始化runtime包、开启GC,最后初始化main包并调用main.main()函数
  6. 由于调用runtime.newproc函数在创建G时会将PC设置为go.exit加一个字节的位置,制造main.mian是被go.exit调用的现象当go routine执行完成会调用runtime.schedule()进行调度循环

抢占机制

同步抢占 : 编译器会在每个函数调用的前面插入一段检查代码。这段代码会检查是否需要进行调度。
如果需要,当前的Goroutine就会被停止,并放回到本地或全局运行队列中,然后运行队列中的另一个Goroutine会被选择出来运行。
而且,这种方法只适用于有函数调用的情况。如果有一个计算密集型的任务没有函数调用(例如一个无限循环),那么这种抢占就无法工作。

异步抢占通过发送信号:为了解决上述问题,Go 1.14引入了异步抢占。运行时系统会定期(大约每10ms)对正在运行的Goroutine发送一个操作系统信号。
当这个信号被接收时,运行时系统会尝试暂停当前Goroutine并执行调度,从而实现抢占。由于这个方法不依赖于函数调用,因此它可以在任何时间点抢占Goroutine,包括那些没有函数调用的长任务。

GC

GoGC是一种并发标记清除算法,使用混合写屏障保证增量标记正确性.

三色标记法

颜色说明
潜在垃圾
待遍历的对象
活跃对象
  1. 将所有对象标记为白色
  2. 每次 GC 回收开始,会从根节点开始遍历所有对象,把遍历到的对象从白色集合放入 “灰色” 集合
  3. 遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合
  4. 重复第三步 , 直到灰色中无任何对象

Go root

  1. 全局数据区的指针
  2. 所有goroutine上面的指针
  3. Finalizer相关变量

混合写屏障

将被覆盖的对象标记成灰色并在当前栈没有扫描时将新对象也标记成灰色

垃圾回收阶段

(1)Sweep Termination,清扫终止,首先Stop the World,从而使所有的P都达到一个GC安全点,然后清扫所有还未清扫的span,通常情况下不会存在还未清扫的span,除非GC提前触发。
(2)Concurrent Mark,并发标记,具体来讲,将gcphase从_GCoff改成_GCmark,启用写屏障和辅助GC,把GC root送入工作队列中。
直到所有的P都开启了写屏障后,GC才会开始标记对象,写屏障的开启也是在STW期间完成的,然后Start the World,GC工作线程和辅助GC会一起完成标记工作,写屏障会将指针赋值过程中被覆盖掉的旧指针和新指针同时着色,新分配的对象会被立即标为黑色。GC root包含所有协程的栈、可执行文件数据段和BSS段等全局数据区,以及来自runtime中一些堆外数据结构里的堆指针。扫描一个goroutine时会先将其挂起,对其栈上发现的指针进行着色,最后恢复它的运行。GC标记时,从工作队列中取出灰色对象,扫描该对象使其变成黑色,并对发现的指针进行着色,这可能又会向工作队列中添加更多指针。因为GC涉及多处本地缓存,所以它使用一种分布式算法来判断所有的GC root和灰色对象都已经处理完,之后会切换至Mark Termination,即标记终止状态。
(3)Mark Termination,标记终止。Stop the World,将gcphase设置成_GCmarktermination,关闭GC工作线程和辅助GC。冲刷所有的mcache,以将mspan还回mcentral中。
(4)Concurrent Sweep,并发清扫。将gcphase设置成_GCoff,重置清扫相关状态并关闭写屏障。Start the World,此后分配的对象就都是白色的了,必要时,分配之前会先对span进行清扫。除了分配时清扫之外,GC还会进行后台清扫。等到分配的内存达到一定的阈值后,又会触发下一轮GC。

触发方式

  • runtime初始化阶段创建的sysmon线程和forcegchelper协程发起,属于基于时间的周期性触发
  • mallocgc()函数发起的,触发条件是堆大小达到或超过了临界值
  • runtime.GC()函数强制触发

标记算法

版本特点
GoV1.3普通标记清除法,整体过程需要启动 STW,效率极低。
GoV1.5三色标记法, 堆空间启动写屏障,栈空间不启动,全部扫描之后,需要重新扫描一次栈 (需要 STW),效率普通
GoV1.8三色标记法,混合写屏障机制, 栈空间不启动,堆空间启动。整个过程几乎不需要 STW,效率较高。

三色标记法

颜色说明
对象的初始状态
待遍历的对象
已遍历对象
  1. 将所有对象标记为白色
  2. 每次 GC 回收开始,会从根节点开始遍历所有对象,把遍历到的对象从白色集合放入 “灰色” 集合
  3. 遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合
  4. 重复第三步 , 直到灰色中无任何对象
  5. 回收所有的白色标记表的对象。也就是回收垃圾

插入屏障

在 A 对象引用 B 对象的时候,B 对象被标记为灰色(在堆空间生效)

删除屏障

被删除的对象,如果自身为灰色或者白色,那么被标记为灰色(在堆空间生效)

混合写屏障

1、GC 开始将栈上的对象全部扫描并标记为黑色 (之后不再进行第二次重复扫描,无需 STW),

2、GC 期间,任何在栈上创建的新对象,均为黑色。

3、被删除的对象标记为灰色。

4、被添加的对象标记为灰色。

GC Root

根节点是指程序中被直接或间接引用的对象集合,它们是垃圾回收器扫描堆中对象时的起点。程序的调用栈中的变量也可以被认为是根节点之一,因为它们可以被其他对象引用

垃圾收集阶段

  1. sweep termination(清理终止)

会触发 STW ,所有的 P(处理器) 都会进入 safe-point(安全点);
清理未被清理的 span

  1. the mark phase(标记阶段)

将 _GCoff GC 状态 改成 _GCmark,开启 Write Barrier (写入屏障)、mutator assists(协助线程),将根对象入队;

恢复程序执行,mark workers(标记进程)和 mutator assists(协助线程)会开始并发标记内存中的对象。对于任何指针写入和新的指针值,都会被写屏障覆盖,而所有新创建的对象都会被直接标记成黑色

GC 执行根节点的标记,这包括扫描所有的栈、全局对象以及不在堆中的运行时数据结构。扫描goroutine 栈绘导致 goroutine 停止,并对栈上找到的所有指针加置灰,然后继续执行 goroutine

GC 在遍历灰色对象队列的时候,会将灰色对象变成黑色,并将该对象指向的对象置灰

GC 会使用分布式终止算法(distributed termination algorithm)来检测何时不再有根标记作业或灰色对象,如果没有了 GC 会转为mark termination(标记终止);

  1. mark termination(标记终止)

STW,然后将 GC 阶段转为 _GCmarktermination,关闭 GC 工作线程以及 mutator assists(协助线程);
执行清理,如 flush mcache;

  1. the sweep phase(清理阶段)
    将 GC 状态转变至 _GCoff,初始化清理状态并关闭 Write Barrier(写入屏障);
    恢复程序执行,从此开始新创建的对象都是白色的
    后台并发清理所有的内存管理单元

协程调度的

  1. runtime 创建最初的线程 m0 和 goroutine g0,并把 2 者关联。
  2. 调用runtime.schedinit()函数,就像它的名字那样,这个函数会初始化调度系统,函数的逻辑较为复杂,相关细节稍后再展开介绍 调度器初始化:初始化 m0、栈、垃圾回收,以及创建和初始化由 GOMAXPROCS 个 P 构成的 P 列表。
  3. )调用runtime.newproc()函数,创建主goroutine,指定的入口函数是runtime.main()函数,这是程序启动后第1个真正的goroutine, 调用runtime.mstart()函数,当前线程进入调度循环。一般情况下线程调用mstart()函数进入调度循环后不会再返回。进入调度循环的线程会去执行上一步创建的goroutine

主goroutine得到执行后,runtime.main()函数会设置最大栈大小、启动监控线程sysmon、初始化runtime包、开启GC,最后初始化main包并调用main.main()函数

由于调用runtime.newproc函数在创建G时会将PC设置为go.exit加一个字节的位置,制造main.mian是被go.exit调用的现象当mian.mian执行完成会继续执行剩余代码调用runtime.schedule()进行调度循环

blog.csdn.net/pengpengzho… 多个协程顺序