Golang协程调度

本文详细介绍了Golang的协程调度机制,包括P、G、M的概念及其作用,以及sched全局调度器的工作原理。调度过程涉及到G的状态管理和切换,重点讨论了系统调用、chan读写和抢占式调度的时机。文章还探讨了协程的多种状态,如Gidle、Grunning、Gsyscall和Gwaiting,以及状态之间的转换。
协程调度
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()需要在进行协程切换时被调用,用来保存被切换出去的协程的信息。

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

### 协程调度机制详解 Golang协程(goroutine)是一种轻量级的用户态线程,由 Go 运行时负责调度。Go 语言通过 G、M、P 三个核心组件构建其调度模型,实现了高效的 M:N 线程调度机制[^1]。 #### G:Goroutine 每个协程在 Go 中以 `G` 表示,它包含了协程的基本信息和执行状态。协程在堆上分配初始化的 2KB 空间,并进入调度流程。这种设计使得协程切换的代价很小,因为只需改变线程执行的位置即可继续运行新的协程[^2]。 #### M:Machine `M` 是操作系统线程的抽象,代表当前运行的内核线程。一个 `M` 可以绑定一个 `P` 并在其上运行多个 `G`。当系统调用是阻塞式或执行时间过久时,会将当前的 `M` 与 `P` 分离,这样可以保证其他协程调度不受影响。系统调用返回后,`M` 会重新获取 `P` 继续进行协程调度[^4]。 #### P:Processor `P` 是处理器的核心概念,主要用于管理协程的执行队列。每个 `P` 都有一个本地运行队列(LRQ),未分配的协程则保存在全局运行队列(GRQ)中,等待被分配给某个 `P` 的 LRQ。Go 调度器采用的是两级线程模型,其中 `P` 在用户态进行调度,而 `M` 则作为内核线程参与实际执行[^5]。 #### 协程的执行流程 当一个新的协程被创建时,它会被放入 GRQ 中。随后,调度器会选择合适的 `P` 将该协程分配到对应的 LRQ 中。一旦 `P` 上的 `M` 成当前任务,就会从 LRQ 中取出下一个协程继续执行。这种机制确保了协程能够在不同的 `M` 上复用,从而提高整体并发性能[^1]。 #### 内核线程 sysmon 的作用 sysmon 是 Go 调度器中的监控线程,负责处理长时间运行的系统调用和其他需要定期检查的任务。如果发现某个 `M` 因为系统调用而长时间不响应,sysmon 会介入并尝试恢复正常的调度流程[^4]。 #### GOMAXPROCS 对并发性能的影响 GOMAXPROCS 参数决定了同时运行的最大 `P` 数量,进而影响程序的并行能力。设置较高的值可能会增加上下文切换开销,而设置较低的值则可能限制真正的并行度。因此,合理配置 GOMAXPROCS 对于优化应用程序性能至关重要。 ```go package main import ( "fmt" "runtime" ) func main() { // 获取当前 GOMAXPROCS 设置 maxProcs := runtime.GOMAXPROCS(-1) fmt.Printf("Current GOMAXPROCS setting: %d\n", maxProcs) // 设置新的 GOMAXPROCS 值 newMaxProcs := 4 runtime.GOMAXPROCS(newMaxProcs) fmt.Printf("Set GOMAXPROCS to: %d\n", newMaxProcs) } ``` 上述代码展示了如何查询和修改 GOMAXPROCS 的值,这可以直接影响 Go 应用程序的并发行为。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值