首页 文章资讯内容详情

Go Runtime

2026-06-01 3 花语

1.Goroutine定义

Goroutine是一个与其他goroutines并行运行在同一地址空间的go函数或方法

一个运行的程序由于一个或更多个goroutine组成.它与线程,协程,进程等不同

他是一个goroutine-RobPike

Goroutines在同一个用户地址空间里并行独立执行functions,channels则用于

Goroutines间的通信和同步访问控制.

2.GMP指的是什么

G(Goroutine):我们所说的协程,为用户级的轻量级线程,每个Goroutine对象中的sched保存着其上下文信息.

M(Machine):对内核级线程的封装,数量对应真实的CPU数(真正干活的对象)

P(Processor):即为G和M的调度对象,用来调度G和M之间的关联关系,其数量可通过GOMAXPROCS()来设置,默认为核心数

3.1.0之前GM调度模型

调度器把G都分配到M上,不同的G在不同的M并发运行时候,都需要向系统中申请资源

比如堆栈内存等,因为资源是全局的,就会因为资源竞争照成很多性能损耗.为了解决这一的问题go从1.1版本引入,在运行时系统的时候加入p对象,让p去管理这个G对象,M想要运行G,必须绑定P,才能运行P所管理的对象

1.单一全局互斥锁(Sched.Lock)和集中状态存储

2.Goroutine传递问题(M经常在M之间传递”可运行”的goroutine)

3.每个M做内存缓存,导致内存占用过高,数据局部性较差

4.频繁syscall调用,导致严重的线程阻塞/解锁,加剧额外的性能损耗。

4、GMP调度流程

每个P有个局部队列,局部队列保存待执行的goroutine(流程2),当M绑定的P的的局部队列已经满了之后就会把goroutine放到全局队列(流程2-1)

每个P和一个M绑定,M是真正的执行P中goroutine的实体(流程3),M

从绑定的P中的局部队列获取G来执行

当M绑定的P的局部队列为空时,M会从全局队列获取到本地队列来执行

G(流程3.1),当从全局队列中没有获取到可执行的G时候,M会从其他P

的局部队列中偷取G来执行(流程3.2),这种从其他P偷的方式称为work

stealing

当G因系统调用(syscall)阻塞时会阻塞M,此时P会和M解绑即hand

off,并寻找新的idle的M,若没有idle的M就会新建一个M(流程5.1)。

当G因channel或者networkI/O阻塞时,不会阻塞M,M会寻找其他

runnable的G;当阻塞的G恢复后会重新进入runnable进入P队列等待执

行(流程5.3)

5、GMP中workstealing机制

存到P本地队列或者是全局队列。P此时去唤醒一个M。P继续执行它的执行

序。M寻找是否有空闲的P,如果有则将该G对象移动到它本身。接下来M执行

一个调度循环(调用G对象->执行->清理线程→继续找新的Goroutine执行)。

6、GMP中handoff机制

当本线程M因为G进行的系统调用阻塞时,线程释放绑定的P,把P转移给其

他空闲的M执行。当发生上线文切换时,需要对执行现场进行保护,以便下次

被调度执行时进行现场恢复。Go调度器M的栈保存在G对象上,只需要将M所

需要的寄存器(SP、PC等)保存到G对象上就可以实现现场保护。当这些寄存器

数据被保护起来,就随时可以做上下文切换了,在中断之前把现场保存起来。

如果此时G任务还没有执行完,M可以将任务重新丢到P的任务队列,等待下

一次被调度执行。当再次被调度执行时,M通过访问G的vdsoSP、vdsoPC寄存

器进行现场恢复(从上次中断位置继续执行)。

7、协作式的抢占式调度

在1.14版本之前,程序只能依靠Goroutine主动让出CPU资源才能触发调

度,存在问题

某些Goroutine可以长时间占用线程,造成其它Goroutine的饥饿

垃圾回收需要暂停整个程序(Stop-the-world,STW),最长可能需要几分钟的时间,导致整个程序无法工作。

8、基于信号的抢占式调度

在任何情况下,Go运行时并行执行(注意,不是并发)的goroutines数量是

小于等于P的数量的。为了提高系统的性能,P的数量肯定不是越小越好,所

以官方默认值就是CPU的核心数,设置的过小的话,如果一个持有P的M,

由于P当前执行的G调用了syscall而导致M被阻塞,那么此时关键点:

GO的调度器是迟钝的,它很可能什么都没做,直到M阻塞了相当长时间以

后,才会发现有一个P/M被syscall阻塞了。然后,才会用空闲的M来强这

个P。通过sysmon监控实现的抢占式调度,最快在20us,最慢在10-20ms才

会发现有一个M持有P并阻塞了。操作系统在1ms内可以完成很多次线程调

度(一般情况1ms可以完成几十次线程调度),Go发起IO/syscall的时候执

行该G的M会阻塞然后被OS调度走,P什么也不干,sysmon最慢要10-20ms

才能发现这个阻塞,说不定那时候阻塞已经结束了,宝贵的P资源就这么被阻

塞的M浪费了

9、GMP调度过程中存在哪些阻塞

I/O,select

blockonsyscall

channel

等待锁

runtime.Gosched()

10、sysmon有什么作用

sysmon也叫监控线程,变动的周期性检查,好处

释放闲置超过5分钟的span物理内存;

如果超过2分钟没有垃圾回收,强制执行;

将长时间未处理的netpoll添加到全局队列;

向长时间运行的G任务发出抢占调度(超过10ms的g,会进行retake);

收回因syscall长时间阻塞的P

11、三色标记原理

我们首先看一张图,大概就会对三色标记法有一个大致的了解:

原理

首先把所有的对象都放到白色的集合中

从根节点开始遍历对象,遍历到的白色对象从白色集合中放到灰色集合中

遍历灰色集合中的对象,把灰色对象引用的白色集合的对象放入到灰色集

合中,同时把遍历过的灰色集合中的对象放到黑色的集合中

循环步骤3,知道灰色集合中没有对象

步骤4结束后,白色集合中的对象就是不可达对象,也就是垃圾,进行回收

12、插入写屏障

golang的回收没有混合屏障之前,一直是插入写屏障,由于栈赋值没有hook

的原因,所以栈中没有启用写屏障,所以有STW。golang的解决方法是:只是

需要在结束时启动STW来重新扫描栈。这个自然就会导致整个进程的赋值器卡

顿,所以后面golang是引用混合写屏障解决这个问题。混合写屏障之后,就

没有STW。

13、删除写屏障

goalng没有这一步,golang的内存写屏障是由插入写屏障到混合写屏障过渡

的。简单介绍一下,一个对象即使被删除了最后一个指向它的指针也依旧可以

活过这一轮,在下一轮GC中被清理掉。

14、写屏障

Go在进行三色标记的时候并没有STW,也就是说,此时的对象还是可以进行修

改。

那么我们考虑一下,下面的情况。

我们在进行三色标记中扫描灰色集合中,扫描到了对象A,并标记了对象A的

所有引用,这时候,开始扫描对象D的引用,而此时,另一个goroutine修改

了D->E的引用,变成了如下图所示

这样会不会导致E对象就扫描不到了,而被误认为为白色对象,也就是垃圾

写屏障就是为了解决这样的问题,引入写屏障后,在上述步骤后,E会被认为

是存活的,即使后面E被A对象抛弃,E会被在下一轮的GC中进行回收,这一

轮GC中是不会对对象E进行回收的。

15、混合写屏障

混合写屏障继承了插入写屏障的优点,起始无需STW打快照,直接并发扫

描垃圾即可;

混合写屏障继承了删除写屏障的优点,赋值器是黑色赋值器,GC期间,任

何在栈上创建的新对象,均为黑色。扫描过一次就不需要扫描了,这样就

消除了插入写屏障时期最后STW的重新扫描栈;

混合写屏障扫描精度继承了删除写屏障,比插入写屏障更低,随着带来的

是GC过程全程无STW;

混合写屏障扫描栈虽然没有STW,但是扫描某一个具体的栈的时候,还是

要停止这个goroutine赋值器的工作的哈(针对一个goroutine栈来

说,是暂停扫的,要么全灰,要么全黑哈,原子状态切换)。

16、GC触发时机

主动触发:调用runtime.GC

被动触发

使用系统监控,该触发条件由runtime.forcegcperiod变量控制,默认为2分

钟。当超过两分钟没有产生任何GC时,强制触发GC。

使用步调(Pacing)算法,其核心思想是控制内存增长的比例。如Go的GC

是一种比例GC,下一次GC结束时的堆大小和上一次GC存活堆大小成比例.

由GOGC控制,默认100,即2倍的关系,200就是3倍,

当Go新创建的对象所占用的内存大小,除以上次GC结束后保留下来的对象占

用内存大小

17、Go语言中GC的流程是什么?

当前版本的Go以STW为界限,可以将GC划分为五个阶段:

阶段说明赋值器状态GCMark标记准备阶段,为并发标记做准备工作,启动写屏

障STWGCMark扫描标记阶段,与赋值器并发执行,写屏障开启并发

GCMarkTermination标记终止阶段,保证一个周期内标记任务完成,停止写屏

障STWGCoff内存清扫阶段,将需要回收的内存归还到堆中,写屏障关闭并发

GCoff内存归还阶段,将过多的内存归还给操作系统,写屏障关闭并发。

18、GC如何调优

通过gotoolpprof和gotooltrace等工具

控制内存分配的速度,限制goroutine的数量,从而提高赋值器对CPU

的利用率。

减少并复用内存,例如使用sync.Pool来复用需要频繁创建临时对象,例

如提前分配足够的内存来降低多余的拷贝。

需要时,增大GOGC的值,降低GC的运行频率。 原文链接: