Golang GMP原理
GPM
多进程和多线程解决了阻塞的问题
但是面临着很多新的问题
-
多个线程之间切换会浪费很多的切换成本
-
进程/线程数量越多,切换成本也就越大,也就越浪费
多线程开发也变得越来越复杂,并且高度消耗cpu
内核态是系统底层
如果将用户和内核分离开,那么会更好一些
操作系统层面是不用改的
在go语言里面用户线程给它起名叫协程
如果一个线程通过协程调度器,绑定多个协程,这样效率会很高
弊端:如果绑定的几个协程中有一个阻塞了,其他的也会受到影响
协程调度器由编程语言自己开发
如果变成1:1的关系那么不会因为阻塞而产生额外的代价,但是所有的协程创建、删除和切换都由CPU完成,有点略显昂贵
协程调度器是语言层级的所以把任务交给协程调度器会更好,尽量减少cpu的消耗,才能提高速度
golang对协程的处理
1、把co-routine改名,并且减少资源量
灵活调度!
golang对早期调度器的处理
老调度器的缺点
- 创建、销毁、调度G都需要每个M获取锁,这就形成了激烈的锁竞争
- M转移G会造成延迟和额外的系统负载
- 系统调用(CPU在M之间的切换)导致频繁的线程阻塞和取消其阻塞操作增加了系统开销
Goroutine调度器的GMP模型的设计思想
GMP模型简介
goroutine:用户线程
process是用来处理协程的,processor包含了goroutine的所有资源,保存了所有的数据,可以通过GOMAXPROOCS来设置数量
P
一个p同一时刻只能执行一个g。程序可以运行的go的数量就是GOMAXPROCS的数量
- p的全局队列
全局队列 存放等待运行的G
- p的本地队列
本地队列存放即将执行的,有数量限制,一半不超过256g
优先将新创建的G放在P的本地队列中,如果满了会放在全局队列中
- p列表
- 程序启动时创建
- 最多有GOMAXPROCS个
m
物理线程(内核级别)
- m列表,当前操作系统分配到当前go程序的内核线程数
P和M的数量
-
P的数量:
- 环境变量$GOMAXPROCS
- 在程序中通过runtime.GOMAXPROCS()
-
M的数量
- Go语言本身时限定了M的最大量是10000个(其实很少有电脑的线程超过10000个)
- 可以通过runtime/debu包中的setmaxthreads函数来设置
- 有一个m阻塞,就会创建一个新的m
- 如果m空闲,那么就会回收或者睡眠
调度器的设计策略
利用线程
work stealing机制
始终保证每个p里面都有g
m1和p绑定。g1正在执行。
要把m2给利用上,因此要从m1里面偷取一个g3
hand off机制
假设g1阻塞
这个时候m1是等待状态,这个时候创建/唤醒一个thread m3,把p1移动到这个thread中,然后g1还是留在m1,这个时候m2和m3正常执行
为了不耽搁g2,会把p和m做一个分离,m1这个时候是一个睡眠或者销毁机制
利用并行
通过GOMAXPROCS限定P的个数
假设:
=CPU核心数/2
抢占
以前的co-routine:
只有c主动释放资源,另外一个c才能获取资源
go-routine
g最多占用10ms。另外的g可以抢占
全局G队列
每个gmp里面提供了一个全局队列,
这个m2这边会先看旁边的队列里面有没有g可以拿到,如果没有的话就从全局队列里面取,取的时候要加锁和解锁
go func经历了什么过程
调度器的生命周期
如果阻塞了
流程
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就会被放入全局队列中
调度器的生命周期
M0
启动程序后编号为0的主线程
在全局变量runtime.m0中,不需要在heap上分配
负责执行初始化操作和启动第一个g
启动第一个g之后,m0就喝其他的m一样了
G0
每次启动一个,,都会第一个创建的gourtine,就是g0
g0仅用于负责调度g
g0不指向任何可执行的函数
每个m都会有一个自己的g0
在调度或者系统调用时会使用m切换到g0,来调度
m0的g0会放在全局空间
以下面的代码为例
package main
import "fmt"
func main(){
fmt.println("Hello World")
}
main也是通过G0来调度
可视化的GMP编程
package main
import (
"fmt"
"os"
"runtime/trace"
)
//trace编程过程
//1、创建文件
//2、启动
//3、停止
func main(){
//1、创建一个trace文件
f,err := os.Create("trace.out")
if err != nil{
panic(err)
}
//2、启动trace
err = trace.Start(f)
if err != nil{
panic(err)
}
//正常的业务
fmt.Println("Hello GMP")
//3、停止trace
trace.Stop()
//通过go tool trace trace.out分析
}
运行go tool trace之后
浏览器会随机生成一个接口
通过Debuge trace查看GMP信息
GODEBUG
GODEBUG=schedtrace=1000 ./trace2
cjp@bogon debug_trace % GODEBUG=schedtrace=1000 ./debug_trace
SCHED 0ms: gomaxprocs=4 idleprocs=1 threads=4 spinningthreads=1 idlethreads=0 runqueue=0 [1 0 0 0]
Hello GMP
SCHED 1006ms: gomaxprocs=4 idleprocs=4 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0]
Hello GMP
SCHED 2015ms: gomaxprocs=4 idleprocs=4 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0]
Hello GMP
SCHED 3015ms: gomaxprocs=4 idleprocs=4 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0]
Hello GMP
SCHED 4024ms: gomaxprocs=4 idleprocs=4 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0]
Hello GMP
# SCHED 调试的信息
# 0ms 从程序启动到输出经历的时间
# gomaxproces P的数量 一般默认是和CPU的核心数量是一致的
# idleprocs 处于idle状态的p的数量gomaxprocess-idleprocess = 目前正在执行的p的数量
# threads 线程数量(包括MD,包括GODEBUG调试的数量)
# spinningthreads 处于自旋状态的thread数量
# idlethreads 处于idle状态的thread
# runqueue 全局G队列中的G的数量
# [0,0] 每个P的local queue本地队列中,目前存在的G的数量
场景1 创建G
这个地方在第九讲
正常来说我们希望G1和G3能够在同一个m中,G3应该优先加入G1所在的本地队列
场景2 G1执行完毕
当G1执行完毕之后会调用goexit
通过g0把g2调过来
m1在执行完g1之后,会优先从本地取G2,通过G0来调度
场景3 G2开辟过多G
会把当前队列进行分割,然后把队伍头部的打乱和G7放到全局队列中
场景 唤醒正在休眠的M
假设G2唤醒了M2,M2绑定了P2,并且开始运行G0,但是P2本地队列没有G,M2此时为自旋线程(没有G但为运行状态的线程,不断寻找G)
场景7 被唤醒的M2从全局队列获取G
min是最小值,GQ是全局队列的长度
场景8 M2从M1中偷取G
G7和G4执行逻辑都比较快,很快就被销毁了,G7执行完了之后M2会继续去G4,如果全部都取完了之后,M2会使用G0来找G(自旋进程)
P2会从P1的尾部偷一个G8
场景9 自旋线程的最大限制
GOMAXPROCS
自旋线程 + 执行线程 <= GOMAXPROCS
场景10 : G发生系统调用/阻塞
目前是M1和M2正在执行,M3M4自旋
突然G8发生syscall阻塞,这个时候会让G8停留在M2中,然后执行G9
这个时候就会把p2移动到M5
为什么不会加到自旋进程M3和M4?
因为自旋线程只会抢占g并不会抢占p(他们已经有了P3和P4),如果此时休眠队列中没有M,那么就会把P2放到空闲P队列中
场景11G发生阻塞/非阻塞
G8这个时候已经不阻塞了
此时P2已经和M5绑定了
M2会记录下来原配是谁,看看能不能抢占,根据场景10的情况,P2已经和M5组合到了一起,所以抢占失败
抢占原来的P失败之后M2会继续去找空闲P队列,结果发现有没有
M2就会放弃寻找,这个时候就会把G8放到全局队列中,然后M2进入到休眠队列
休眠队列里面的M如果长期不被执行,就会被GC回收