Go Runtime
1. goroutine定义
golang在语言级别支持协程,称之为goroutine; golang标准库提供的所有系统调用操作(包括所有同步I/O操作)
都会让出CPU给其它goroutine, 这让goroutine的切换管理不依赖于系统的线程和进程,也不依赖于CPU的核心数量
而是交给Golang的运行时统一调度
2. GMP指的是什么
* G(goroutine),我们所说的协程,用户级的轻量级线程,每个goroutine对象中的sched保存着其上下文信息
* M(machine),对内核级线程的封装,数量对应真实的CPU数量(真正干活的对象)
* P(processor),即为G和M的调度对象,用来调度G和M之间的关联关系,其数量可通过GOMAXPROCS来设置,默认是CPU核心数量
3. 1.0之前GM调度模型
调度器把不同的G分配到M上,不同的G在不同的M并发运行时,都需要向系统申请资源,比如堆栈内存,因为资源是全局的
就会因为资源竞争造成很多性能损耗,为了解决这一问题go从1.1版本引入,在运行时系统的时候加入p对象,
让P去管理G对象,M想要运行G,必须先绑定P,然后才能运行P下面的G对象
GM调度存在的问题:
1. 单一全局互斥锁(Sched.Lock)和集中状态存储
2. groutine传递问题(M经常在M之间传递可运行的goroutine)
3. 每个M做内存缓存,导致内存占用过高,数据局部性较差
4. 频繁的syscall调用, 导致严重的线程阻塞/解锁,家具额外的性能损耗_
4. GMP调度流程
1. 每个p有个局部队列,局部队列放的是待执行的goroutine,当M绑定的P的局部队列满了以后就会把G放到全局队列
2. 每个P和一个M绑定,M是真正执行P中goroutine的实体,M从绑定的P的局部队列中获取G来执行
3. 当M绑定的P的局部队列为空时,M就会从全局队列获取到本地队列来执行,当全局队列也为空时,
M就会从其它P队列偷取G来执行,这种从其它P偷的方式称之为 work stealing
4. 当G因系统调用(syscall)阻塞时会阻塞M,此时P会和M解绑即hand off,并寻找新的M,如果没有空闲的M就会新建一个M
5. 当G因channel或者network I/O操作阻塞时,不会阻塞M,M会寻找其它runable的G,当阻塞的G恢复后重新进入runable进入P队列等待执行
5. GMP中的work stealing机制
先获取p本地队列,如果为空时,去全局队列里取G运行,如果全局队列为空时从netpoll和事件池里拿
如果在拿不到从其它P队列里偷
6. GMP中的handoff机制
当本地线程M因为G进行的系统调用阻塞时,会释放绑定的P,把P转移给其它空闲的M执行
7. 协作式的抢占式调度
在1.14之前,程序只能依靠goroutine主动让出CPU资源才能触发调用,这种方式存在问题有:
1. goroutine长时间占用线程,造成其它goroutine的饥饿
2. 垃圾回收需要暂停整个程序,最长可能几分钟,导致整个程序无法工作
8. 基于信号的抢占式调度
1. 在任何情况下,go运行时并行执行的goroutine数量要小于等于p的数量
2. 为了提高性能,p的数量肯定不是越小越好,官方给出默认是CPU的核心数量
3. 如果设置过小的,当M绑定的P执行的G执行系统调用阻塞,导致M也阻塞时,GO的调度器是迟钝的,他有可能什么都不做
它有可能什么都不做,知道M阻塞了相当长时间以后,才会发现一个P/M被syscall阻塞了,然后才会用空闲的M来抢这个P
所以P也不建议设置太小,通过sysmon监控实现的抢占式调度,最快在20us,最慢在10-20ms才会发现一个M持有P并阻塞了,
而操作系统1ms可以完成几十次系统调度
所以P适当的比CPU核心数多一些最好
9. GMP调度过程中存在哪些阻塞
1. I/O, select
2. block on syscall
3. channel
4. 等待所
5. runtime.Gosched()
10. sysmon有什么用
sysmon也叫监控线程,变动的周期性检查,好处
1. 释放超过5分钟的span物理内存
2. 如果超过两分钟没有垃圾回收,强制执行
3. 将长时间未处理的netpoll加入到全局队列
4. 向长时间运行的g任务发出抢占调度(超过10ms的g,会进行retake)
5. 收回因syscall阻塞的P