cpu切换浪费成本
线程可以有三中状态
等待中(waiting)
待执行(Runnable)
执行中(Executing)
线程可以做两种类型的工作。第一个称为 CPU-Bound,第二个称为 IO-Bound。
CPU-Bound:这种工作类型永远也不会让线程处在等待状态,因为这是一项不断进行计算的工作。比如计算 π 的第 n 位,就是一个 CPU-Bound 线程。
IO-Bound:这是导致线程进入等待状态的工作类型。比如通过网络请求对资源的访问或对操作系统进行系统调用。
go协程放入全局队列,M获取和放回要枷锁和解锁
当系统执行异步的网路调用的时候,会使用网络轮询器的东西(netpoll)来更有效地处理系统调用,此时,会将G交给网络轮训器来执行
M会和P解绑定,M阻塞等G的执行,P在下一个G启动的时候寻找新的M的绑定执行,当此M执行完毕会优先寻找之前绑定的P,如果此时P不处于空闲状态,M查找其他的P,如果没有空闲的P。M会将G放入全局队列,等带执行
调度器介入后:识别出**G1
已导致M1
阻塞,此时,调度器将M1
与P
分离,同时也将G1
带走。然后调度器引入新的M2
来服务P
。此时,可以从 LRQ 中选择G2
并在M2
**上进行上下文切换。
阻塞的系统调用完成后:**G1
可以移回 LRQ 并再次由P
**执行。如果这种情况需要再次发生,M1将被放在旁边以备将来使用。
在runtime.main
中会创建一个额外m运行sysmon
函数,抢占就是在sysmon中实现的。
sysmon会进入一个无限循环, 第一轮回休眠20us, 之后每次休眠时间倍增, 最终每一轮都会休眠10ms. sysmon中有netpool(获取fd事件), retake(抢占), forcegc(按时间强制执行gc), scavenge heap(释放自由列表中多余的项减少内存占用)等处理。
调用 handoffp
解除 M 和 P 的关联。
设置标识,标识该函数可以被中止,当调用栈识别到这个标识时,就知道这是抢占触发的, 这时会再检查一遍是否要抢占。
sysmon不和任何的P绑定,是单独运行,负责G的监控及抢占
sysmon函数是Go runtime启动时创建的,负责监控所有goroutine的状态,判断是否需要GC,进行netpoll等操作。sysmon函数中会调用retake函数进行抢占式调度。
关于扫描周期,至少是20us
一个循环,后面视idle
循环次数来进行指数退避
(超过1ms之后倍增),但最长时间不超过10ms
,故系统至多在10ms
左右进行一次抢占检测.
也就是说当sysmon检测到M被阻塞了10ms,就会解绑M和P,然后别的M抢占P进行执行
有计数来记录P的调度次数,还会记录上次执行的时间,如果下次检测P调度次数没有增加,则将当前时间更新,然后将P和M绑定,将当前的G和M绑定执行
t := int64(_p_.schedtick)
if int64(pd.schedtick) != t { //在周期内已经调度过,即当前p上运行的g改变过.
pd.schedtick = uint32(t)
pd.schedwhen = now //更新最近一次抢占检测的时间
continue
}
if pd.schedwhen+forcePreemptNS > now {
continue
}
preemptone(_p_)
从上面关键数据结构得知 p.schedtick 记录了这个P上总共调度次数(递增), 故sysmon
通过比较最近一次记录的schedtick
即可判断在一个周期内是否发生过调度行为.
通过最近一次检测时间与当前时间比较来明确是否需要抢占标记pd.schedwhen+forcePreemptNS>now
forcePreemptNS
为10ms
,如果超过10ms没有调度,则需要抢占, PS:并不能保证一个G最多运行10ms.
最后通过preemptone
来标记当前G需要被抢占
func preemptone
注释已经说了(runtime的注释很好),抢占触发时机: 目标g进行函数调用中触发栈检测过程中进行.
func testfunc()(sum int){
var nums[100] int
for _, num := range nums {
sum += num
}
return
}
避免频繁的创建、销毁线程,而是对线程的复用。
1)work stealing机制
调度或者系统调用
时,会使用M切换到G0,来调度M0的G0,会放在全局空间[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qj0tMM0a-1608540238460)(https://img.kancloud.cn/b3/10/b31027eeb493fa86654b41d46f34a98b_439x872.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-898A88da-1608540238461)(GMP.assets/image-20201215204818497.png)]
package main
import (
"fmt"
"log"
"os"
"runtime/trace"
)
func main() {
//创建文件
f, err := os.Create("trace.out")
if err != nil {
log.Fatal(err)
}
defer f.Close()
//启动
err = trace.Start(f)
fmt.Println("hello")
trace.Stop()
}
go run -race test.go
打开文件
go tool trace trace.out
2020/12/15 20:53:44 Parsing trace...
2020/12/15 20:53:44 Splitting trace...
2020/12/15 20:53:44 Opening browser. Trace viewer is listening on http://127.0.0.1:54494
package main
import (
"fmt"
"time"
)
func main() {
for i := 0; i < 50; i++ {
time.Sleep(1 * time.Second)
fmt.Println("hello")
}
}
go build -o test2 test.go
GODEBUG=schedtrace=1000 ./test2
SCHED 0ms: gomaxprocs=8 idleprocs=5 threads=4 spinningthreads=1 idlethreads=0 runqueue=0 [1 0 0 0 0 0 0 0]
hello
SCHED 1004ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
hello
SCHED 2012ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
hello
SCHED 3017ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
hello
SCHED 4022ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
hello
SCHED 0ms: gomaxprocs=8 idleprocs=5 threads=4 spinningthreads=1 idlethreads=0 runqueue=0 [1 0 0 0 0 0 0 0]
hello
gomaxprocs=8 最大线程数
idleprocs=5 空闲的线程数
threads=4 使用线程数
spinningthreads=1 自旋线程
idlethreads=0 空闲线程
runqueue=0 [1 0 0 0 0 0 0 0] 第一个0 全局队列 然后是每个本地队列G的数量
当G1创建G3,为了保持局部性,优先加入G1所在的本地队列
M优先从自己的本地队列获取G2执行
假设只能存4个G,那么由G2创建的G也会加入到本地队列
将本地队列从1/2划分,将头部的打乱和新创建的加入到全局队列,尾部的往前推
假设只能存4个G,那么由G2创建的G也会加入到本地队列
假设只能存4个G,那么由G2创建的G也会加入到本地队列
将本地队列从1/2划分,将头部的打乱和新创建的加入到全局队列,尾部的往前推,保证优先度一样
将本地队列从1/2划分,将头部的打乱和新创建的加入到全局队列,尾部的往前推,保证优先度一样
每创建一个G的时候,尝试唤醒休眠队列的的一个M(前提是休眠M队列有M),然后和空闲的P绑定,没有的话,重新回到休眠队列
此时绑定了M的P就是自旋线程,会从别处偷取G执行
唤醒的M2,从全局队列获取G执行
调用G0切换到G3,执行,此时就不是自旋线程了
全局队列获取的个数`
n = min(len(GQ)/GOMAXPROCS+1,len(GQ/2))
GQ 全局队列G
M2被唤醒之后是自旋线程,全局队列位空
此时从其他队列偷取一半,的后半部分来到自己的本地队列执行
GOMAXPROCESS控制P的数量
最大的自旋线程数为GOMAXPROCESS-不是自旋的线程数
其他线程放入休眠线程池中
当M2发生系统调用或者网络请求阻塞的时候,M2会和P2解绑
解绑后的P2会寻找是否有空闲的M,如果有,就和其绑定,没有放入空闲P队列中
当阻塞的G和M2不阻塞之后,M2必须有P才可以执行G,优先获取P2
此时P2和P5绑定,那么会从空闲的P队列中获取是否有空空闲的P
如果没有,那么G会和M2解绑,将G放入全局队列
如果休眠线程队列长期没有被唤醒,就会被GC回收
https://www.kancloud.cn/aceld/golang/195830
刘丹冰大佬 欢迎大家支持大佬