目录

Golang GMP原理

GPM

多进程和多线程解决了阻塞的问题

但是面临着很多新的问题

  • 多个线程之间切换会浪费很多的切换成本

  • 进程/线程数量越多,切换成本也就越大,也就越浪费

    https://cdn.cjpa.top/cdnimages/image-20201220115043898.png

多线程开发也变得越来越复杂,并且高度消耗cpu

https://cdn.cjpa.top/cdnimages/image-20201220115217839.png

https://cdn.cjpa.top/cdnimages/image-20201220115247189.png

内核态是系统底层

如果将用户和内核分离开,那么会更好一些

https://cdn.cjpa.top/cdnimages/image-20201220115312556.png

操作系统层面是不用改的

https://cdn.cjpa.top/cdnimages/image-20201220115338417.png

在go语言里面用户线程给它起名叫协程

如果一个线程通过协程调度器,绑定多个协程,这样效率会很高

弊端:如果绑定的几个协程中有一个阻塞了,其他的也会受到影响

协程调度器由编程语言自己开发

https://cdn.cjpa.top/cdnimages/image-20201220115446963.png

如果变成1:1的关系那么不会因为阻塞而产生额外的代价,但是所有的协程创建、删除和切换都由CPU完成,有点略显昂贵

https://cdn.cjpa.top/cdnimages/image-20201220115834984.png

协程调度器是语言层级的所以把任务交给协程调度器会更好,尽量减少cpu的消耗,才能提高速度

https://cdn.cjpa.top/cdnimages/image-20201220115848500.png

golang对协程的处理

1、把co-routine改名,并且减少资源量

https://cdn.cjpa.top/cdnimages/image-20201220120026357.png

灵活调度!

golang对早期调度器的处理

https://cdn.cjpa.top/cdnimages/image-20201220120121442.png

https://cdn.cjpa.top/cdnimages/image-20201220120216525.png

老调度器的缺点

  • 创建、销毁、调度G都需要每个M获取锁,这就形成了激烈的锁竞争
  • M转移G会造成延迟和额外的系统负载
  • 系统调用(CPU在M之间的切换)导致频繁的线程阻塞和取消其阻塞操作增加了系统开销

https://cdn.cjpa.top/cdnimages/image-20201220121114118.png

Goroutine调度器的GMP模型的设计思想

GMP模型简介

goroutine:用户线程

process是用来处理协程的,processor包含了goroutine的所有资源,保存了所有的数据,可以通过GOMAXPROOCS来设置数量

https://cdn.cjpa.top/cdnimages/image-20201220121913610.png

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

https://cdn.cjpa.top/cdnimages/image-20201220140043865.png

m1和p绑定。g1正在执行。

要把m2给利用上,因此要从m1里面偷取一个g3

https://cdn.cjpa.top/cdnimages/image-20201220140152283.png

hand off机制

假设g1阻塞

https://cdn.cjpa.top/cdnimages/image-20201220140220541.png

这个时候m1是等待状态,这个时候创建/唤醒一个thread m3,把p1移动到这个thread中,然后g1还是留在m1,这个时候m2和m3正常执行

https://cdn.cjpa.top/cdnimages/image-20201220140336982.png

为了不耽搁g2,会把p和m做一个分离,m1这个时候是一个睡眠或者销毁机制

利用并行

通过GOMAXPROCS限定P的个数

假设:

​ =CPU核心数/2

https://cdn.cjpa.top/cdnimages/image-20201220140525451.png

抢占

以前的co-routine:

只有c主动释放资源,另外一个c才能获取资源

go-routine

g最多占用10ms。另外的g可以抢占

https://cdn.cjpa.top/cdnimages/image-20201220140646982.png

全局G队列

每个gmp里面提供了一个全局队列,

https://cdn.cjpa.top/cdnimages/image-20201220140846611.png

这个m2这边会先看旁边的队列里面有没有g可以拿到,如果没有的话就从全局队列里面取,取的时候要加锁和解锁

go func经历了什么过程

调度器的生命周期

https://cdn.cjpa.top/cdnimages/image-20201220141319573.png

https://cdn.cjpa.top/cdnimages/image-20201220141333810.png

https://cdn.cjpa.top/cdnimages/image-20201220141403218.png

https://cdn.cjpa.top/cdnimages/image-20201220141434824.png

https://cdn.cjpa.top/cdnimages/image-20201220141620856.png

如果阻塞了

流程

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")
}

https://cdn.cjpa.top/cdnimages/image-20201220152828108.png

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之后

https://cdn.cjpa.top/cdnimages/image-20201220154127944.png

https://cdn.cjpa.top/cdnimages/image-20201220154255489.png

https://cdn.cjpa.top/cdnimages/image-20201220154725104.png

浏览器会随机生成一个接口

通过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执行完毕

https://cdn.cjpa.top/cdnimages/image-20201220165915424.png

当G1执行完毕之后会调用goexit

通过g0把g2调过来

https://cdn.cjpa.top/cdnimages/image-20201220170012775.png

m1在执行完g1之后,会优先从本地取G2,通过G0来调度

场景3 G2开辟过多G

https://cdn.cjpa.top/cdnimages/image-20201220170226506.png

https://cdn.cjpa.top/cdnimages/image-20201220170246218.png

会把当前队列进行分割,然后把队伍头部的打乱和G7放到全局队列中

https://cdn.cjpa.top/cdnimages/image-20201220170425448.png

场景 唤醒正在休眠的M

https://cdn.cjpa.top/cdnimages/image-20201220170621506.png

https://cdn.cjpa.top/cdnimages/image-20201220170746475.png

https://cdn.cjpa.top/cdnimages/image-20201220170752331.png

https://cdn.cjpa.top/cdnimages/image-20201220170822031.png

假设G2唤醒了M2,M2绑定了P2,并且开始运行G0,但是P2本地队列没有G,M2此时为自旋线程(没有G但为运行状态的线程,不断寻找G)

场景7 被唤醒的M2从全局队列获取G

https://cdn.cjpa.top/cdnimages/image-20201220171147089.png

min是最小值,GQ是全局队列的长度

场景8 M2从M1中偷取G

https://cdn.cjpa.top/cdnimages/image-20201220171447757.png

G7和G4执行逻辑都比较快,很快就被销毁了,G7执行完了之后M2会继续去G4,如果全部都取完了之后,M2会使用G0来找G(自旋进程)

P2会从P1的尾部偷一个G8

https://cdn.cjpa.top/cdnimages/image-20201220171603385.png

场景9 自旋线程的最大限制

GOMAXPROCS

自旋线程 + 执行线程 <= GOMAXPROCS

https://cdn.cjpa.top/cdnimages/image-20201220171821092.png

场景10 : G发生系统调用/阻塞

https://cdn.cjpa.top/cdnimages/image-20201220171939214.png

目前是M1和M2正在执行,M3M4自旋

突然G8发生syscall阻塞,这个时候会让G8停留在M2中,然后执行G9

https://cdn.cjpa.top/cdnimages/image-20201220172023187.png

这个时候就会把p2移动到M5

https://cdn.cjpa.top/cdnimages/image-20201220172133733.png

为什么不会加到自旋进程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回收