Golang深入学习之GPM模型

0、引言

一些相关知识

  • 进程占用内存:虚拟内存4GB(32bit OS)
  • 线程占用内存:大约4MB

演进过程

  • 并发执行各种线程,切换线程会造成较大的性能损耗,多线程的同步竞争(锁、竞争资源冲突等)问题,最后还有上面说到的占用内存较大
  • 系统分为内核空间用户空间,可以通过这个思想,将线程分割开来作为一个个协程co-routine,内核空间存放内核线程,用户空间存放切割后的协程,中间通过协程调度器来维持工作。
  • 所以说,M:N的调度工作就交给了协程调度器,线程层面(即内核空间)语言控制不了,所以说语言间的区别主要在于协程调度器的设计

Goroutine

  • Golang将co-routine封装,自己命名为Goroutine,内存占用个位数KB,这样就可以大量产生自己想要的协程。
  • Golang编写自己的调度器,使得调度更加灵活,所以GPM出现。

1、GPM

简介

  • G:goroutine 协程
  • P:processor 处理器
  • M:thread 内核线程

结构

  • 全局G队列:存放等待的G
  • P的本地队列:同样存放等待运行的G,存放限制数量不超过256个,优先将新创建的G存入,若已满则存入全局队列
  • P列表:许多P组成的列表,程序启动时创建,限制数量为GOMAXPROCS(可设置)。
  • M列表:当前操作系统分配到当前OS程序的内核线程数,限制数量为10000,也可通过runtime/debug包中的SetMaxThreads设置。若有一个M阻塞,则创建一个新的M,若有空闲M,则回收或者睡眠。

设计策略

  • 复用线程
    work stealing 机制:若某个M处于空闲状态,则可以去偷取别的M对应P队列中等待的G任务来执行。
    hand off 机制:若某个P队列中G任务阻塞,则会唤醒一个新的M,将P队列剩余G任务移入新的M,阻塞的G则等待,对应阻塞的M则回收或休眠
  • 利用并行:通过GOMAXPROCS限定P的个数,空出一些P给别的程序使用。
  • 抢占:若GA占用CPU时,GB来请求使用,则GA最多使用10ms后释放CPU,再次排队使用。
  • 全局G队列:对于work stealing的补充,空闲的M先去全局G队列中拿取,若全局G队列为空,则再执行work stealing机制去别的M中偷取。(注:全局队列会有锁机制,需要加锁解锁进行偷取)。

M0&G0

  • M0: 程序启动时OS分配给程序的编号0的主线程,在全局变量runtime.m0中,不需要在heap上分配,负责执行初始化和启动第一个G,启动第一个G之后,M0就和其他的M一样了
  • G0:每个M启动时,都会先创建一个goroutine,它就是G0。G0负责调度M中的各种G任务,从不指向任何可执行的函数,在调度或者系统调用时,M都会先切换到G0,然后通过G0来调度其 他G任务
  • M0G0会放在全局空间。

上代码(可视化GPM调试)

func main() {
	// 创建trace文件
	traceFile, err := os.Create("trace.out")
	if err != nil {
		log.Fatalln(err.Error())
		return
	}
	defer traceFile.Close()

	// 启动trace
	err = trace.Start(traceFile)
	if err != nil {
		log.Fatalln(err.Error())
		return
	}

	// 逻辑代码
	fmt.Println("Hello GPM~")

	// 停止trace
	trace.Stop()
}
  • 编译运行后会得到一个trace.out文件,然后通过go tool trace工具可以分析trace文件,返回一个url通过浏览器可视化,如下图。

Golang深入学习之GPM模型_第1张图片

2、场景

创建G

  • 局部性原则:由一个G1创建一个新的G3时,优先将新的G3放入和G1相同的本地队列中

G执行完毕

  • 当本地队列一个G执行完毕后,先切换为G0,然后通过G0先去调用本地队列中的G任务。

G开辟过多的G

  • 如果一个G申请创建过多的G,超过了本地队列的最大值,根据创建G的场景,会先依次将本地队列存满,这时本地队列已满,但还有新创建G的需求,比如G8,那么就会将本地队列一分为二,将前一部分的移入全局队列,后一部分往前移 ,并将G8也移入全局队列,此时已有空闲空间,若还有新的G申请创建,则正常放入本地队列即可,循环反复。

唤醒正在休眠的M

  • 每次新创建一个G时,运行的G会尝试唤醒其他空闲的P和M组合去执行。假定唤醒了M2,M2绑定了P2,此时M2会运行自身的G0,但P2队列本身没有G任务,那么M2此时就为自旋线程没有G任务,但是在运行状态的线程,会不断地寻找G,先从全局G队列中拿取,全局队列为空时,再去别的进程偷取,并且为批量偷取,一分为二,将后半部分全部偷取,总共自旋线程获取数量n符合如下公式)。

n = min(len(GQ) / GOMAXPROCS + 1, len(GQ / 2))

自旋线程的最大限制

  • 自旋线程 + 执行线程 <= GOMAXPROCS
  • 多余没事儿做的线程会进行休眠处理,而不是自旋

G发生系统调用/阻塞

  • 假设当前M1和M2在工作,M3和M4为自旋线程,M5和M6为休眠队列中的休眠线程,即最大线程数为4。当前M2对应的P2中的G1发生系统调用/阻塞且队列中还有G2任务,则M2和P2立即解绑,P2会执行以下判断:如果存在P2本地队列中还有G任务全局G队列中有G任务休眠线程队列中有空闲的M,则P2都会立马唤醒一个M与其绑定,否则P2则会加入到空闲P列表,等待M来获取可用的P。上例中,P2本地队列中还有G2任务,则可以与空闲线程队列中的M5进行绑定,继续后续工作。

G从阻塞变为非阻塞

  • 接上例,若G1从阻塞变为非阻塞,此时对应的M2会优先尝试获取原先的P2,若P2已被绑定在别的M,则从空闲P队列中获取,若空闲队列中为空,则M2获取P失败,直接将M2放入休眠线程队列G变为可运行状态,放入全局G队列中。

你可能感兴趣的:(Golang,golang,多线程,并发编程)