思维导图:
1. 发展过程
思维导图:
在单机时代是没有多线程、多进程、协程这些概念的。早期的操作系统都是顺序执行
单进程的缺点有:
- 单一执行流程、计算机只能一个任务一个任务进行处理
- 进程阻塞所带来的CPU时间的浪费
时间片轮训算法
并发执行
这样的好处是充分利用了CPU,但是也带来了一些问题,例如时间片切换需要花费额外的开销
成本就越大浪费
锁竞争冲突
创建、切换、销毁进程调度
所以提高cpu的利用率成为我们需要解决的问题
线程上下文切换
io流的读写操作用户态和内核态
这个时候我们的线程模型是这样的
内核态用户态
做好绑定堵塞
内核线程
用户线程协程(co-runtine)
一比一
N 比 1
event-loop
M 比 N
M 比 N协程调度器
协程调度器协程内存
早期的协程调度器队列MG全局G队列M加锁互斥/同步全局G队列互斥锁
老调度器有几个缺点:
激烈的锁竞争延迟和额外的系统负载G'GG'M'很差的局部性G'GM'
2. GMP模型设计思想
思维导图:
2.1 GMP模型
goroutine协程machineprocessor处理器
在Go中,线程是运行goroutine的实体,调度器的功能是把可运行的goroutine分配到工作线程上
GG256G'G'GPGOMAXPROCSPGPM全局队列GPPP
$GOMAXPROCSruntime.GOMAXPROCS()一万runtime/debugSetMaxThreadsM阻塞新的MM空闲回收或者休眠
2.2 调度器的设计策略
golang调度器的设计策略思想主要有以下几点:
- 复用线程
- 利用并行
- 抢占
- 全局G队列
2.2.1 复用线程
work stealing机制hand off机制
work stealing充分利用线程进行并行计算,并减少了线程间的竞争
双端队列被窃取任务头部窃取任务尾部
M1睡眠或销毁
2.2.2 利用并行
GOMAXPROCSGOMAXPROCSGOMAXPROCSGOMAXPROCS = 核数/2
2.2.3 抢占策略
co-routinegoroutine
2.2.4 全局G队列
-
全局G队列其实是复用线程的补充,当工作窃取时,优先从全局队列去取,取不到才从别的p本地队列取(1.17版本)
-
在新的调度器中依然有全局G队列,但功能已经被弱化了,当M执行work stealing从其他P偷不到G时,它可以从全局G队列获取G
go func()
go func()G局部调度器P全局G队列本地队列全局的队列MP1:1可执行状态的Gsyscall其他阻塞摘除(detach)空闲的P本地队列休眠状态空闲线程全局队列
2.4 调度器的生命周期
M0G0编号为0runtime.m0heap启动第一个GM0就和其他的M一样了启动一个M第一个创建的gourtineG0负责调度G可执行的函数
具体流程为:
我们来分析一段代码:
package main
import "fmt"
func main() {
fmt.Println("Hello world")
}
PP列表main.mainruntimeruntime.mainruntime.mainmain.mainruntime.mainmain.mainruntime.mainruntime.exit
runtime.mainruntime.mainruntime.main
2.5 可视化的CMP编程
2.5.1 trace方式
go tool trace trace.out
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)
}
defer func(f *os.File) {
err := f.Close()
if err != nil {
panic(err)
}
}(f)
// 2. 启动trace
err = trace.Start(f)
if err != nil {
panic(err)
}
// 正常要调试的业务
fmt.Println("hello GMP")
// 3. 停止trace
trace.Stop()
}
view trace
G的信息:
M的信息:
P的信息:
2.5.2 debug方式
使用debug方式可以不需要trace文件
先搞一段代码
package main
import (
"fmt"
"time"
)
func main() {
for i := 0; i < 5; i++ {
time.Sleep(time.Second)
fmt.Println("hello GMP")
}
}
debug执行一下
$ GODEBUG=schedtrace=1000 ./debug.exe
SCHED 0ms: gomaxprocs=8 idleprocs=7 threads=6 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
SCHED 1008ms: gomaxprocs=8 idleprocs=8 threads=6 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
hello GMP
SCHED 2009ms: gomaxprocs=8 idleprocs=8 threads=6 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
hello GMP
SCHED 3010ms: gomaxprocs=8 idleprocs=8 threads=6 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
hello GMP
hello GMP
SCHED 4017ms: gomaxprocs=8 idleprocs=8 threads=6 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
hello GMP
SCHED0msgomaxprocsidleprocsthreads: os threads/Mspinningthreadsidlethreadrunqueue=0[0 0]
3. GMP场景分析
3.1 G1创建G3
go func()
3.2 G1执行完毕
goexitscheduleexecute
3.3 G溢出
假设每个P的本地队列只能存3个G。G2要创建了6个G,前3个G(G3, G4, G5)已经加入p1的本地队列,p1本地队列满了
本地队列G饥饿
这些G被转移到全局队列时,会被打乱顺序。所以G3,G4,G7被转移到全局队列。
G2创建G8时,P1的本地队列未满,所以G8会被加入到P1的本地队列
3.4 唤醒正在休眠的M
规定:在创建G时,运行的G会尝试唤醒其他空闲的P和M组合去执行
自旋线程
3.5 自旋线程获取G
n = min(len(GQ) / GOMAXPROCS + 1, cap(LQ) / 2 )
自旋线程
相关源码参考:
// 从全局队列中偷取,调用时必须锁住调度器
func globrunqget(_p_ *p, max int32) *g {
// 如果全局队列中没有 g 直接返回
if sched.runqsize == 0 {
return nil
}
// per-P 的部分,如果只有一个 P 的全部取
n := sched.runqsize/gomaxprocs + 1
if n > sched.runqsize {
n = sched.runqsize
}
// 不能超过取的最大个数
if max > 0 && n > max {
n = max
}
// 计算能不能在本地队列中放下 n 个
if n > int32(len(_p_.runq))/2 {
n = int32(len(_p_.runq)) / 2
}
// 修改本地队列的剩余空间
sched.runqsize -= n
// 拿到全局队列队头 g
gp := sched.runq.pop()
// 计数
n--
// 继续取剩下的 n-1 个全局队列放入本地队列
for ; n > 0; n-- {
gp1 := sched.runq.pop()
runqput(_p_, gp1, false)
}
return gp
}
3.6 M2从M1中偷取G
全局队列已经没有G,那m就要执行work stealing(偷取):从其他有G的P哪里偷取一半G过来,放到自己的P本地队列。P2从P1的本地队列尾部取一半的G,本例中一半则只有1个G8,放到P2的本地队列并执行。
- 偷取队列元素的一半
3.7 自旋线程的最大限制
G1本地队列G5、G6已经被其他M偷走并运行完成,当前M1和M2分别在运行G2和G8,M3和M4没有goroutine可以运行,M3和M4处于自旋状态,它们不断寻找goroutine
- 正在运行的M + 自旋线程 <= GOMAXPROCS
- 如果M大于P,则进入休眠线程队列
GOMAXPROCSGOMAXPROCS
3.8 G发送系统调用/阻塞
假定当前除了M3和M4为自旋线程,还有M5和M6为空闲的线程(没有得到P的绑定,注意我们这里最多就只能够存在4个P,所以P的数量应该永远是M>=P,大部分都是M在抢占需要运行的P),G8创建了G9,G8进行了阻塞的系统调用,M2和P2立即解绑,P2会执行以下判断:如果P2本地队列有G、全局队列有G或有空闲的M,P2都会立马唤醒1个M和它绑定,否则P2则会加入到空闲P列表,等待M来获取可用的p。本场景中,P2本地队列有G9,可以和其他空闲的线程M5绑定
- 自旋线程抢占G,不抢占P
3.9 G发送系统调用/非阻塞
G8空闲p队列M阻塞队列