协程调度
P
// P的状态
const (
// P status
_Pidle = iota
_Prunning // P的状态只能从_Prunning改变
_Psyscall
_Pgcstop
_Pdead
)
程序中的P会在程序启动的时候初始化完成并链接在sched全局调度器的pidle队列中。
G
// 协程状态
const (
// G status
_Gidle = iota // 空闲的G
_Grunnable // 可以被调度的G
_Grunning // 正在被调度的G
_Gsyscall // 陷入系统调用的G
_Gwaiting // 阻塞中的G
_Gdead // 未使用的G
// ......
)
创建一个g对象时,g会加入到本地或者全局队列。
M
sysmon线程:该线程不用绑定P即可运行,内部是一个死循环:
- 检查是否所有的协程都已经锁死,如果是的话直接调用runtime.throw强制退出,这个操作只在启动的时候做一次。
- 将netpoll返回的结果注入到全局的任务队列
- 收回因为进入系统调用二长时间阻塞的P,同时抢占那些执行时间过长的g
- 如果span的内存闲置超过5min,则释放掉
sched全局调度器
M
|
------------P
| |
_________________ |
| | | | | | | | | |
g g g g g g g g g G
首先空闲的m会被链接进全局调度器的midle队列,当需要m的时候,会首先从这里取出m,如果此时没有可以使用的m,则调用newm来创建一个新的m。
//src/runtime/runtime2.go
type g struct {
......
m *m
atomicstatus uint32 //记录g的状态
}
type m struct {
......
curg guintptr // 当前正在被M运行的G
p puintptr // 当前M绑定的P结构
oldp puintptr // 陷入系统调用之前与m绑定的p
......
}
type p struct {
......
M muintptr // 当前P绑定的M
......
// 当前可以运行的G队列
runqhead uint32
runqtail uint32
runq [256]guintptr
// 可以被重新使用的G队列
gFree struct {
gList
n int32
}
......
}
type schedt struct {
midle muintptr // 空闲的m组成的链表
nmidle int32 // number of idle m's waiting for work
nmidlelocked int32 // number of locked m's waiting for work
mnext int64 // number of m's that have been created and next M ID
maxmcount int32 // maximum number of m's allowed (or die)
nmsys int32 // number of system m's not counted for deadlock
ngsys uint32 // number of system goroutines; updated atomically
pidle puintptr // 空闲的P组成的链表
npidle uint32
// 全局的runnable状态的g队列
runq gQueue
runqsize int32
// ...
}
调度过程
- 程序创建的G会被均匀分配在已经存在的P或者全局的Grunable队列上。
- P在程序启动的时候会进行初始化,自golang1.5开始,GOMAXPROCS的默认值为CPU的核数。
- 当程序中存在可以被调度的P时,会先去寻找空闲的M与自身绑定,如果此时没有空闲的M,则就回去创建新的M。
- M会从绑定的 P的本地队列、sched中的全局队列、netpoll中获取可运行的 G
调度时机
系统调用时机
当运行的G陷入系统调用时,如果当前正在运行的G陷入阻塞或执行时间过久时,P的状态会该P的状态修改为Gsyscall,调度器会将P与当前M解绑,让当前的M与发生系统调用的G完美运行
放弃M的P会去哪里?
在golang的调度机制中,有一个特殊的M线程(sysmon),专门用来接收被抛弃的P,sysmon线程会周期性的醒来遍历所有的P,当发现有状态为Psyscall的P并且已经保持该状态一段时间,sysmon线程就会为当前的P重新分配一个M来执行
当被放弃的M结束系统调用之后会去绑定之前属于自己的P,如果此时sysmon线程还没有为P分配新的M,则P会与M重新绑定;当此时P已经与新的M进行绑定时,此时的M就会将自己挂起,等待系统分配新的空闲P
chan读写时机
当g发生向channel中写数据发现channel已经满的时候,P就会将当前的g挂起(分配到等待队列),并进行一次调度,知道有g对该channel进行读取时发现有阻塞写的g时会再次将被挂起的g唤醒
抢占式调度
sysmon协程除了检测处于Psyscall状态的P之外,还会检查处于Prunning状态的P,来避免某一个g占有M,并在某个时间剥夺该g进行调度,CPU密集型协程占有M的问题
协程状态
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gtx48CEm-1608610855937)(https://s2.ax1x.com/2019/07/08/ZrLLRJ.png)]
创建协程从Gidle开始
Grunnable:处于该状态的协程随时都可能被调度器(P&M)获取并执行
Grunning:该状态下的协程表明当前正在执行,在golang1.2之前的版本,一个正在运行的任务需要通过调用yield的方式显示的让出处理器;在go1.2之后开始支持一定程度的任务抢占->当系统线程发现某一个任务执行的时间过长或者在进行垃圾回收时,会将任务设置为"可被抢占",当该任务下一次调用函数时,就会让出处理器并重新回到Grunnable状态
Gsyscall:Go为了保证高的并发性能,在执行系统调用时会先调用runtime.entersyscall将当前任务状态该为Gsycall,因为系统调用的权限更高,Go的调度器是在用户状态下的调度器,所以无法控制系统调用的执行,所以当系统调用是阻塞式或者执行时间过久时,就会将当前的M与P分离,M代表当前的运行线程,P则代表当前线程可以调度的协程任务队列,这表示P将被其他的M接管,所以当前的系统调用不会影响其他协程的调度。当系统调用返回后,执行线程M通过runtime.exitsyscall重新获取P继续进行P协程调度
Gwaiting:处于该状态的任务可以被看作处阻塞状态,只有当条件满足时,才可以进入Grunnable状态被调度运行,golang中的定时器、网络IO、chan都会导致协程处于当前状态。
Gdead:协程运行结束会调用runtime.goexit将状态设置为Gdead,并将结构体链接到一个属于当前P的空闲G链表中,以备后续使用。
协程状态切换
tls线程局部存储,可见性仅限于当前线程中,getg()用来获得当前线程正在执行的g。mcall()需要在进行协程切换时被调用,用来保存被切换出去的协程的信息。
调用时机:系统调用返回 协程阻塞 抢占式调度