一、golang调度器的由来

1、单进程时代(单任务系统)

 

       顺序执行任务,同一时刻只有一个进程被执行;

       进程阻塞带来cpu资源的浪费。

2、多进程多线程时代(多任务系统)

       任务并发执行,轮询调度,每个进程执行一个时间片。

       上下文切换的切换成本增大,cpu利用率降低。

       多线程随着同步竞争(锁、竞争资源冲突)开发设计变得越来越复杂。

       占用内存资源大,进程占用内存,虚拟内存4GB(32bit操作系统),线程约4MB。

3、协程时代(内核线程+用户线程)

       通过语言级别的协程调度器,调度较多的用户线程,来协调绑定较少的内核线程,减少了内核线程的切换开销。

       早期调度器只有一个全局队列,多个m从同一个全局队列竞争g,造成激烈的锁竞争。新创建的g被放入全局队列导致被其他m执行,破坏了局部性。

二、GMP模型

1、GMP 模型

  • Processor

       它包含了运行 goroutine 的资源,如果线程想运行 goroutine,必须先获取 P,P 中还包含了可运行的 G 队列。

  • 全局队列(Global Queue)

       存放等待运行的 G。

  • P本地队列

       存放的也是等待运行的 G,不超过 256 个。新建 G’时,G’优先加入到 P 的本地队列,如果队列满了,则会把本地队列中一半的 G 移动到全局队列。

       从 P 本地可运行队列先选出一个可运行的 goroutine;为了公平,调度器每调度 61 次的时候,都会尝试从全局队列里取出待运行的 goroutine 来运行,调用 globrunqget;如果还没找到,就要去其他 P 里面去偷一些 goroutine 来执行

  • P 列表

       所有的 P 都在程序启动时创建,并保存在数组中,最多有 GOMAXPROCS(可配置) 个。

  • M

       线程想运行任务就得获取 P,从 P 的本地队列获取 G,P 队列为空时,M 也会尝试从全局队列拿一批 G 放到 P 的本地队列,或从其他 P 的本地队列偷一半放到自己 P 的本地队列。M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去。

       Goroutine 调度器和 OS 调度器是通过 M 结合起来的,每个 M 都代表了 1 个内核线程,OS 调度器负责把内核线程分配到 CPU 的核上执行。

2、有关 P 和 M 的个数问题

  • P 的数量:

       由启动时环境变量 $GOMAXPROCS 或者是由 runtime 的方法 GOMAXPROCS() 决定。这意味着在程序执行的任意时刻都只有 $GOMAXPROCS 个 goroutine 在同时运行。

  • M 的数量

       go 语言本身的限制:go 程序启动时,会设置 M 的最大数量,默认 10000. 但是内核很难支持这么多的线程数,所以这个限制可以忽略。

       runtime/debug 中的 SetMaxThreads 函数,设置 M 的最大数量

       一个 M 阻塞了,会创建新的 M。

       M 与 P 的数量没有绝对关系,一个 M 阻塞,P 就会去创建或者切换另一个 M,所以,即使 P 的默认数量是 1,也有可能会创建很多个 M 出来。

3、P 和 M 何时会被创建

  • P 何时创建

       在确定了 P 的最大数量 n 后,运行时系统会根据这个数量创建 n 个 P。

  • M 何时创建

       没有足够的 M 来关联 P 并运行其中的可运行的 G。比如所有的 M 此时都阻塞住了,而 P 中还有很多就绪任务,就会去寻找空闲的 M,而没有空闲的,就会去创建新的 M。

4、sysmon后台监控线程做了什么

       到 p 对应的 m 处于系统调用之中到现在已经超过 10 毫秒。这说明系统调用所花费的时间较长,需要对其进行抢占,以此来使得 retake 函数返回值不为 0,这样,会保持 sysmon 线程 20 us 的检查周期,提高 sysmon 监控的实时性。

三、gmp模型的设计思想

1、调度器的设计策略

       并行、抢占、复用线程(偷取 + 分离)、减少全局竞争

  • 并行

       采用M:N模型,通过多核多线程来实现并行。通过GOMAXPROCS 限定P的个数,默认为逻辑cpu数量。

  • 抢占

       限定G与M的绑定时间,最多10ms,超过时间解绑,避免其他协程饥饿。

  • 复用线程(偷取 + 分离)

       work stealing 机制(PM偷G):自旋线程(有P无G的M,只有G0),尝试从与其他M绑定的P队列中取队尾一半。避免浪费M资源。

       hand off 机制(PM分离):M阻塞,P与M分离,创建/唤醒一个休眠线程接管P,避免其他协程饥饿。

  • 减少全局竞争

       优先从本地队列调度G,而不是全局队列,减少全局竞争。

2、go func 的调度过程

       创建:创建一个G对象

       放置:优先放入创建G的线程的本地P队列中,如果本地队列已满,则将当前队列的前半部分和新创建的G一起放入全局队列中。

       唤醒:顺便唤醒一个M(如果有空闲P,没有自旋线程,且当前创建者不是main),如果一个M被唤醒,则尝试和一个空闲的新P进行绑定,如果没有空闲P队列,M回到休眠状态。

       获取:M尝试从本地P队列中获取G,如果本地P队列中没有G,尝试从全局P队列批量获取G放入本地P队列,或者从其他P队列中批量偷取后一半G放入本地P队列。取不到则M成为自旋线程,自旋超时则成为休眠线程,休眠超时则被GC回收销毁。

       调度:调度G,执行,时间片超时或执行结束返回,G被放回本地队列或销毁,M调度执行下一个G。

       系统调用或阻塞:如果执行G时发生系统调用(syscall)或者阻塞(锁、channel),如果有其他G在执行,从休眠的M队列唤醒一个M,或者创建一个新的M,接管当前M的P和P队列。

       系统调用退出或阻塞返回:当阻塞结束,M被放入休眠M队列,G被放入全局P队列。       

3、M0与G0

       M0:启动程序后编号为0的主线程,保存在全局变量runtime.m0中,不需要再heap上分配,负责初始化和启动第一个G,启动一个G之后,M0就和其他M一样了

       G0:启动M时创建的第一个G,负责调度G,G0不指向任何可执行的函数,每个M都会有一个自己的G0,在调度和系统调用时,M会先切换到G0,然后通过G0切换到其他G,来调度M0的G0会放在全局空间。

  

四、gmp调度场景分析

1、创建G

       新创建的G优先加入本地队列,保证局部性。 

2、创建过多的G

       本地放满后,将当前队列的前半部分和新创建的G一起打乱放入全局队列中。

 3、创建G时,唤醒正在休眠的M

  

        在创建一个G的时候,尝试从休眠的M队列中唤醒一个M。如果一个M被唤醒,新M尝试和一个空闲的新P进行绑定,如果没有空闲P队列,M回到休眠状态。

       如果新唤醒的M与空闲的P绑定成功,则调度G0,从本地队列获取G,如果本地队列为空,此时M为自旋线程(本地无G但为运行状态,不断寻找G)。

4、自旋线程M优先从全局队列批量获取G

       优先从全局队列获取G,n = min(len(GlobalQueue) / GOMAXPROCS, len(GlobalQueue) / 2),称为从全局队列到本地队列的负载均衡。

 5、自旋线程从其他队列批量偷取G

       自旋线程优先从全局队列获取G,全局队列为空时,尝试从其他队列偷取后一半。

6、M获取执行G

       M优先从本地队列获取G,并且切换G时,通过G0来调度(先切换到G0,再切换到其他G。G0负责调度时协程的切换,函数sehedule),实现线程的复用

 7、自旋线程的最大限制

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

       多余的线程应该放入休眠线程队列。

8、G发生系统调用或者阻塞

 

        G发生系统调用或者阻塞时,P和M分离,从休眠的M队列唤醒一个M,或者创建一个新的M,接管当前M的P和P队列。

 9、退出系统调用或阻塞结束

        退出系统调用或阻塞结束,M尝试抢占原先的P,抢占失败,尝试从空闲P队列获取P,如果都没有,G会被放入全局队列,M会被放入休眠队列(长期休眠等待GC回收销毁)。

五、调试

1、go tool trace

(1)编写测试代码

package main

import (
	"fmt"
	"os"
	"runtime/trace"
)

/*
	go run trace.go
	go tool trace trace.out
*/

func main() {
	//1. 创建一个trace文件
	f, err := os.Create("trace.out")
	if err != nil {
		panic(err)
	}
	defer f.Close()

	//2. 启动trace
	err = trace.Start(f)
	if err != nil {
		panic(err)
	}

	//3. 执行程序
	fmt.Println("hello gmp trace")

	//4. 关闭trace
	trace.Stop()
}

(2)运行 go 文件

       go run trace.go

(3)解析 trace 文件

       go tool trace .\trace.out

Parsing trace...
Splitting trace...
Opening browser. Trace viewer is listening on http://127.0.0.1:6066

Trace Viewer is running with WebComponentsV0 polyfill, and some features may be broken. As a workaround, you may try running chrome with "--enable-blink-features=ShadowDOMV0,CustomElementsV0,HTMLImports" flag. See crbug.com/1036492.

配置环境变量,然后加启动参数,通过终端启动chrome:

chrome --enable-blink-features=ShadowDOMV0,CustomElementsV0,HTMLImports

 

 (4)参数

  • View trace:查看跟踪,能看到一段时间内 goroutine 的调度执行情况,包括事件触发链;

  • Goroutine analysis:Goroutine 分析,能看到这段时间所有 goroutine 执行的一个情况,执行堆栈,执行时间;

  • Network blocking profile:网络阻塞概况(分析网络的一些消耗)

  • Synchronization blocking profile:同步阻塞概况(分析同步锁的一些情况)

  • Syscall blocking profile:系统调用阻塞概况(分析系统调用的消耗)

  • Scheduler latency profile:调度延迟概况(函数的延迟占比)

  • User defined tasks:自定义任务

  • User defined regions:自定义区域

  • Minimum mutator utilization:Mutator 利用率使用情况

2、GODEBUG

GODEBUG=schedtrace=1000 ./trace.out