Golang协程调度

协程调度
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-bq7VgbWC-1608700675684)(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()需要在进行协程切换时被调用,用来保存被切换出去的协程的信息。

调用时机:系统调用返回 协程阻塞 抢占式调度

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值