Runtime 简介和发展
Runtime 简介
Golang Runtime 是go语言运行所需要的基础设施
- 协程调度,内存分配,GC
- 操作系统以及CPU相关的操作的封装
- Proof,trace,race检测
- Map, channel,string等内置类型以及反射实现
- 同python和java不同,Go没有虚拟机的概念,runtime直接被编译成native code
- Go的runtime和用户代码一起打包在一个可执行的文件中
- go 对系统调用的指令进行了封装,可以不依赖glibc
Golang调度简介
理解调度,需要首先理解两个概念:运行和阻塞。特别是在协程中,这两个概念不容易被正确理解。正确的理解我们应该处理事情的时候像是CPU,而不是而不是像线程或者协程. 假如我当前在写某个服务, 发现依赖别人的函数还没有 ready, 那就把写服务这件事放一边. 点开企业微信, 我去和产品沟通一些问题了. 我和产品沟通了一会后, 检查一下, 发现别人已经把依赖的函数提交了, 然后我就最小化企业微信, 切到 IDE, 继续写服务 A 了.
go 在用户态实现调度, 所以 go 要有代表协程这种执行流的结构体, 也要有保存和恢复上下文的函数, 运行队列. 理解了阻塞的真正含义, 也就知道能够比较容易理解, 为什么 go 的锁, channel 这些不阻塞线程.
真正代表协程的是 runtime.g 结构体. 每个 go func 都会编译成 runtime.newproc 函数, 最终有一个 runtime.g 对象放入调度队列. 上面的 func1 函数的指针设置在 runtime.g 的 startfunc 字段, 参数会在 newproc 函数里拷贝到 stack 中, sched 用于保存协程切换时的 pc 位置和栈位置.
协程切换出去和恢复回来需要保存上下文, 恢复上下文, 这些由以下两个汇编函数实现. 以上就能实现协程这种执行流, 并能进行切换和恢复.(下图中的 struct 和函数都做了精简)
GPM模型
数据结构 | 数量 | 意义 | |
---|---|---|---|
G | Runtime.g运行的函数指针,stack,上下文等 | 无限制 | 代表用户的代码执行流 |
P | Runtime.P runs,free g 等 | 默认是机器的核数 | 表示执行需要的资源 |
M | Runtime.m 对应一个由clone创建的线程 | 比P多,最大一万多 | 代表执行者,底层线程 |
- mcache 从M移动到P中
- 不在是单独的runq, 每个P拥有自己的runq,新的g放入自己的runq,满了后在会放入全局的runq,优先从自己的runq获取g执行
- 实现work stealing,当某个P的runq中没有可以执行的G的时候,会从全局获取需要执行的G
- 当G因为网络或是锁切换,那么G和M分离,M通过调度执行新的G
- 当M因为系统调用阻塞或是cgo运行一段时间后,sysmon协程辉将P和M分离,由其他的M来结合P进行调度。
首先为什么把全局队列打散, 以及 mcache 为什么跟随 P, 这个在 GM 模型那一页就讲的比较清楚了.然后为什么 P 的个数默认是 CPU 核数: Go 尽量提升性能, 那么在一个 n 核机器上, 如何能够最大利用 CPU 性能呢? 当然是同时有 n 个线程在并行运行中, 把 CPU 喂饱, 即所有核上一直都有代码在运行.
在 go 里面, 一个协程运行到阻塞系统调用, 那么这个协程和运行它的线程 m, 自然是不再需要 CPU 的, 也不需要分配 go 层面的内存. 只有一直在并行运行的 go 代码才需要这些资源, 即同时有 n 个 go 协程在并行执行, 那么就能最大的利用 CPU, 这个时候需要的 P 的个数就是 CPU 核数. (注意并行和并发的区别)
调度
golang调度的职责就是为需要执行的Go的代码(G)寻找执行者(M)以及执行的准许和资源(P),并没有一个调度实体,调度是需要发生在带哦度时由m执行runtime.schedule方法进行。
调度的时机
- channel ,mutex等sync操作发生了协程阻塞
- Time.sleep
- 网络操作暂时未ready
- g c
- 主动yield
- 运行过久或是系统调用过久
Sysmon 协程
P的数量影响了同时运行GO的协程数,如果P被占用的过久,就会影响调度。sysmon协程的一个功能就是进行抢占
sysmon的协程是在go runtime 初始化后,执行用户编写的代码之前,由runtime的启动不在与任何P的绑定,直接由一个M执行的协程,类似于Linux的一些执行系统任务的内核线程。
- 每次sysmon运行都会执行一次抢占,如果某个P的G执行超过一个sysmon tick,则执行一次抢占,正在执行系统调用的话,将P和M脱离,正在执行GO代码则通过抢占
- 每两分钟如果没有执行GC,则通知gchelper协程执行一次GC
网络
用户态的协程:结合epoll,nonblock模式的fd操作,网络操作没有好的时候,切换协程,如果网络请求好了,后把相关协程添加到运行队列。保证网络操作达到既不阻塞线程,又是同步的执行效果
- 封装epoll,有网络操作的时候会epollcreate一个epfd
- 所有网络fd均通过fcntl设置为NONBLOCK模式,以边缘触发模式方式epol l节点中
- 对网络f d执行fd 的非阻塞模式, 对于没有 ready 的非阻塞 fd 执行网络操作时, linux 内核不阻塞线程, 会直接返回 EAGAIN, 这个时候将协程状态设置为 wait, 然后 m 去调度其他协程.
- go 在初始化一个网络 fd 的时候, 就会把这个 fd 使用 epollctl 加入到全局的 epoll 节点中. 同时放入 epoll 中的还有 polldesc 的指针.
- 在 sysmon 中, schedule 函数中, start the world 中等情况下, 会执行 netpoll 调用 epollwait 系统调用, 把 ready 的网络事件从 epoll 中取出来, 每个网络事件可以通过前面传入的 polldesc 获取到阻塞在其上的协程, 以此恢复协程为 runnable.
调度综述
- 轻量级协程,栈初始为2kB,调度不涉及系统调度
- 调度是计算机中分配工作锁需要资源的方法,linux的调度为CPU找到可运行的线程,而Go的调度为M找到P和可执行的G
- 用户函数调用前会检查栈的空间是否足够,不够的话,会进行2倍的扩容,最大的1G,超出为painic
- 用户代码中的协程同步造成的阻塞,仅仅是切换g,而不阻塞线程,m 和p仍然结合,去寻找需要执行的G
- 每个P均有local runs,大多数时间只是会和local runq进行无锁交互,新生成的G放入到local runq中
- Sysmon:对于运行比较久的G设置抢占标示,对于过久的syscall的P,进行m和P分离,防止p被占用过久影响调度
内存分配简介
- 类似于TCMallc结构
- 使用span机制来减少碎片,每个span至少为一个页,每一种span用于一个范围内的数据存储,比如6-32byte使用32byte的span
- 一共有67个size范围
- 多层cache来减少分配冲突
- mheap中以treap的结构维护空闲的page,归还内存到heap时,连续地址会进行合并
- stack分配也会多层和多class的
- 对象由GC进行回收,sysmon会定时将空余的内存归还给操作系统
GC简介
- 并发和并行:通常在GC领域中,并发收集器是说来集回收的同时应用程序也在执行,并行收集期说说垃圾回收采集多个线程利用多个CPU一起进行GC。
- safePoint:安全点,是说收集器能够识别出线程执行栈上的所引用的一点或是一段时间
- stop the world:某些垃圾回收算法或是某个极端进行的时候,需要将应用程序完全暂停
- Mark:从root对象开始扫描,标记出其中一个引用对象,这些对象引用的对象,如此循环可以标记出所有的对象
- Compact:压缩的方式是将存活对象移动到一起来获得一段连续的空闲空间,也叫做重定位,这样需要将所有对象的引用指向新的位置,工作量和存活对象量成正比
- Sweep:清除阶段扫描区域,回收在标记阶段标记为DEAD的对象,通常通过空闲链表(free list)的方式,需要的工作量和堆大小成正比。
- Copy:复制算法将所有存活对象从一个From区域移动到另外一个区域,然后回收这个区域,工作量和存活对象量成正比。
三色标记法
- 有黑白灰三个集合,初始化所有对象是白色
- 从ROOT对象开始标记,并将所有可以达到的对象标记为灰色
- 从灰色对象集合中取出对象,将其引用的对象标记为灰色,放入灰色集合,并将自己标记为黑色
- 重复第三步,直到灰色集合清空
- 标记结束,不可达的白色对象为垃圾对象,对内存进行清扫,并回收空间
- 重置GC状态
优化建议
- 涉及文件,或是CGO文件较多的程序,可以将P增大
- 什么时候需要协程池
- 主要还是要隔离减少栈的扩容和缩容,大量维持连接的协程不可以不用协程栈
- 复杂任务使用协程
- 全局缓存有大量key的情况下,少用指针
- 一点点拷贝胜过传指针
- slice 和map 的容量初始化:减少不断的加元素的扩容
- Json-iterator 带地encoding/json
- 集成gops和开启pprof
Runtime 总结
思想 | 作用 | 实例 |
---|---|---|
并行 | 减少操作的wall time和阻塞 | |
纵向多层次 | 减少锁竞争和冲突,更加细粒度的锁控制 | |
横向多个class | 找到最适配的,减少内存浪费和碎片化 | |
缓存 | 减少重新申请 | |
缓冲 | 放入队列,操作异步化 | |
均衡 | 负载均衡,不会因为work太多而成为瓶颈 |
总结于知乎:https://zhuanlan.zhihu.com/p/95056679
有疑问加站长微信联系(非本文作者)