写点什么

GO 训练营第 12、13 周—— runtime

用户头像
Glowry
关注
发布于: 2021 年 03 月 19 日

Goroutine 原理

模型

gorouine 可以理解为比线程轻量级的“线程”,轻量指的是切换、创建、销毁消耗小,内存占用少;且采用线程/goroutine = M:N 模式,同一时刻线程只能处理一个 goroutine,其它是暂停的,但通过一定的调度逻辑使多个 goroutine 并发执行,这样的模式能够让多个线程托管大量的 goroutine。

GMP 调度


G 是 goroutine;M 指的是线程;P 是 CPU 的抽象,主要有个队列功能,所以可以把它当队列看。

早期因为是全局锁的方式去调度 M 和 G,后来为了性能考虑引入了 P,它的数量决定了并行运行的线程任务数量,P 会在队列(本地或全局)有任务(goroutine 等待执行)时创建或调度一个 M 来执行。

另外 P 的本地队列为了避免锁的性能消耗,使用了 lock-free 的方法。

M 的调度策略通俗描述:没 P 找 P,有 P 找 G。

存在的问题:

goroutine 没有关联性的优先级,可能会存在多个 goroutine 等一个核心条件的 goroutine 完成才能继续,这时候无法得到合理的调度。

Working-stealing

由于有多个 P,且其有本地队列,加上全局队列,调度可能会出现以下问题:

如果 P 只执行其本地队列,则某个 P 任务少的话则会空闲;

如果本地队列都比较满,那全局队列就没 P 接管,则会产生饥饿;

解决它们需要 Working-stealing 算法,它指导空闲的 P 去尝试获取别的 P 的本地队列,且平时会时不时到全局队列拿任务来处理。

Syscall

如果 goroutine 执行了 syscall,则会使 M 也处于阻塞状态(因为实际干活的操作系统的线程),此时的 P 也会进入 syscall 状态,暂时不绑定其它的 M,等待一小段时间,如果在此期间 M 处理完成则可以在不用切换数据上下文的情况下完成系统调用,保证数据的局部性(因为每个 CPU 有自己的缓存,如果线程切换了 CPU 就需要缓存的同步,降低性能),如果“超时了”则会把 P 分配给其它的 M,这个“超时”策略是由 sysmon 负责的。

go 没有限制阻塞调度的 goroutine 数量,即没有限制阻塞的线程数量,所以用阻塞的系统调用时需要考虑操作系统线程耗尽的问题。

线程自旋

如果 M 要找 P 运行,会自旋查找队列,这样会消耗一定的 CPU 资源,但也能提高调度的效率,一小段时间的自旋是比较通常的做法。

Network poller

go 的 I/O 都是阻塞的,但它仅是阻塞 goroutine,不会阻塞线程;

被 I/O 阻塞的 G 在 schedule, sysmon, start the world 的情况下被重新调度;

优点: goroutine 里的写法都是同步的,代码可读性好;

Scheduler Affinity

在队列里等待的 G 一般是 FIFO 的,但有些后进的 G 已经先从阻塞中恢复了,可以通过 P 的 runnext 机制提高其被调度的优先级。


Goroutine Lifecycle

start

启动 m0 主线程,初始化 g0 负责 schedule,初始化 P, sysmon 线程, GC 协程;

然后 P 负责创建 os thread 关联 M;

G 切换时,暂存当前的 PC 及 GO 堆栈即可;

回收:G 用完会放到空闲列表,由 P 来负责回收。


内存分配原理

堆栈

栈由编译器决定,分配无锁较轻量,一般是局部变量;

堆由编译器与开发者共同管理分配,需要配合 GC 回收,分配需加锁较昂贵;

局部变量如果较大,也会被分配在堆上;

内存逃逸

原本是栈上的变量,需要在栈外时则会分配在堆上,即变量的作用域从当前栈内变为栈外的域;

下图举了个栈上变量逃逸到堆的例子:

分析方法:go build -gcflags '-m'

原则:应该尽量减少内存逃逸,减轻 GC 压力和内存碎片;


连续栈

问题:程序运行需要的栈空间有时会超出 goroutine 初始化的栈大小,需要进行栈扩容。

解决:

go1.3 之前使用分段栈,即栈被分成多个段,像链表一样串起来。但存在 hot split 问题,即程序的栈空间变化大,一直在扩段和缩段之间频繁切换,导致频繁的 alloc/free,导致性能问题。

go1.3 之后采用连续栈,即栈不够时通过拷贝数据到更大的连续栈空间来扩容栈,且会在栈空间使用率较小时自动缩小栈空间。

栈空间扩缩需要在运行时检测栈的长度,这是在编译时候通过插入 runtime.morestack 来判断,不是每个函数都插入,会根据当前剩余的栈长度判断,且通过几种栈阈值尽量减少计算,保证性能损失最小化。


内存结构

问题:

内存碎片;

内存申请要加锁,锁开销大,影响性能;


借鉴 TCMolloc 的设计;

分为 mspan, mcache, mcentral, mheap, 系统内存;

mspan 为 go 内存的最小分配块;

mspan 包含多个 page,每个 page 是 8KB,page 会根据 sizeclass(内存规格)均分为多个 object,且 sizeclass 有多种类型,如 8byte, 16byte, 32byte 等,go 会根据当前所需对象大小来分配;

mcentral 管理全局线程的 mspan;

mcache 管理当前 P 的 mspan;

如果 mcache 内存不够,则通过 mcentral 申请 mspan;

如果 mcentral 内存不够,则向 mheap 申请,如果 mheap 也不够就向系统内存申请;


使用:

分配 16B 以下的内存对象使用 tiny;

分配 32KB 以下的内存对象使用上述多层级管理;

分配 32KB 以上的直接到 mHeap 申请。


优点:

在 mcache 中,因为一个 P 同时刻只运行一个 goroutine,所以分配内存是无锁的;

mspan, page, object 的形式,为对象分配的特定长度的内存,避免内存碎片;


缺点:

object 内存存储完对象后可能还剩余一些空间,造成利用率下降。


优化实践

• 小对象结构体合并:结构体嵌入值对象,只初始化一次,减少对象数量

• bytes.Buffer

• slice、map 预创建:避免内存数据迁移、创建、销毁;

• 避免长调用栈导致的栈扩容

• 避免频繁创建临时对象

• 字符串拼接 strings.Builder

• 不必要的 memory copy:writev, readv

• 分析内存逃逸

GC 原理

Mark & Sweep

mark, 标记,标记追踪式算法;

sweep,标记没有被使用的对象将被“清理”;基于 span 中的位图 gcmarkBits 来判断是否能被清理;有即时清理和分配时清理两种方式;

两者是并行执行的;

存在的问题:

早期使用暂停 STW(stop the world,暂停其它所有线程的运行)的方式,安全地标记内存使用,但对性能影响很大。

Tri-color Mark & Sweep

三色标记法,黑为活跃对象,灰为活跃对象但存在引用白色对象,白为不活跃对象。

整个过程是:首先所有节点是白,从根节点出发,先把根染为灰色,下一步再把灰色染为黑,把灰的白色染为灰,直到所有对象都染色。

Write Barrier

写屏障+删屏障 = 混合屏障


Channel 原理

一般作为多个 goroutine 通信的通道。

特点

goroutine 安全;

在 goroutine 间存储或传输数据;

消息先进先出;

缓冲或无缓冲通道会引起 goroutine 非阻塞或阻塞;


原理

以下是 chan 的核心结构

circular queue 表示环形队列,是 chan 存数据的结构,当队列满了,停止接收数据,阻塞发送的操作;

send index, receive index 分别表示接下来要发送或接收的数据在环形队列中的链表,存储了等待中的 goroutine 及数据引用,相当于为两个 goroutine 的栈数据打通了直接访问的通道,这样的好处是,数据不需要重新到环形队列获取,提高了效率;

mutex 是互斥锁,发送和接收都是通过加锁实现的,也保证了当前 chan 只能有一个 goroutine 在通信,比如接收或发送;

chan 对象在堆内存,创建的 chan 对象是引用类型,所以作为函数参数时不需要使用指针类型就能访问到实参的数据;

使用

参考:https://www.ardanlabs.com/blog/2017/10/the-behavior-of-channels.html


经验

chan 和 proc 的代码值得看;

挑感兴趣的先看原理再看源码;

go 性能优化相关文章:

fasthttp:

https://mp.weixin.qq.com/s/7gaSxmA1aqfxfbGpMeZK-A

goroutine 池:

https://www.jianshu.com/p/8f8abb504267

https://www.cnblogs.com/williamjie/p/9267741.html

工具:

https://github.com/dgryski/go-perfbook

https://cch123.github.io/perf_opt/


用户头像

Glowry

关注

还未添加个人签名 2019.02.13 加入

还未添加个人简介

评论

发布
暂无评论
GO训练营第12、13周—— runtime