理解 Golang 中 Goroutine 调度机制
in Note with 0 comment
理解 Golang 中 Goroutine 调度机制
in Note with 0 comment

前言

Goroutine 是 Go语言中的一种轻量级线程,它可以在单个线程上运行,通过使用协程来实现并发。Goroutine 可以在不阻塞主线程的情况下执行任务,从而提高程序的效率和并发性能。事实上每一个 Go 程序至少有一个 Goroutine:main Goroutine。Go 程序从 main 包的main()函数开始,在程序启动时,Go 程序就会为main()函数创建一个默认的goroutine。 如果需要了解更多可以查看我另外一篇文章:here

通过关键字go启用多个协程:

go f(x)

那么 Go 是如何保证 Goroutine 执行的高效率和并发性?

答案是:Go runtime 的调度器。goroutine 建立在操作系统线程基础之上,它与操作系统线程之间实现了一个多对多(M:N)的两级线程模型。

这里的 M:N 是指 M 个 goroutine 运行在 N 个内核线程之上,内核负责对这 N 个操作系统线程进行调度,而这N个系统线程又通过 goroutine 调度器负责对这 M 个 goroutine 进行调度和运行。

GM 模型

Go1.0 的协程是 GM 模型

2019-10-31T08:23:58.png

M(内核线程)想要执行、放回 G 都必须访问全局 G 队列,并且 M 有多个,即多线程访问同一资源需要加锁进行保证互斥/同步,所以全局 G 队列是有互斥锁进行保护的。

这个调度器有几个缺点:

GPM 模型

从 Go1.2 开始,新的协程调度器引入了P(Processor),成为了完善的GPM模型。Processor,它是处理器(Process)的抽象,但不是真正的CPU,能够分配CPU 资源的还是M。它包含了运行 goroutine 的资源,如果线程想运行goroutine,必须先获取P,P中还包含了可运行的 G 队列。

2019-10-31T08:39:44.png

上图中各个模块的作用如下:

P 是怎样处理上面的问题的呢?

策略

队列轮转

每个P维护着一个包含G的队列,不考虑G进入系统调用或IO操作的情况下,P周期性的将G调度到M中执行,执行一小段时间,将上下文保存下来,然后将G放到队列尾部,然后从队列中重新取出一个G进行调度。

除了每个P维护的G队列以外,还有一个全局的队列,每个P会周期性的查看全局队列中是否有G待运行并将期调度到M中执行,全局队列中G的来源,主要有从系统调用中恢复的G。之所以P会周期性的查看全局队列,也是为了防止全局队列中的G被饿死。

线程自旋

调度器会保证至少有一个M在自旋检查P和G有没有可绑定的,避免任务等待,但自旋也会浪费一些CPU算力。

系统调用

P的个数默认等于CPU核数,每个M必须持有一个P才可以执行G,一般情况下M的个数会略大于P的个数,这多出来的M将会在G产生系统调用时发挥作用。类似线程池,Go也提供一个M的池子,需要时从池子中获取,用完放回池子,不够用时就再创建一个。

当M运行的某个G产生系统调用时,如下图所示:

2019-10-31T08:42:29.png

如图所示,当G0即将进入系统调用时,M0将释放P,进而某个空闲的M1获取P,继续执行P队列中剩下的G。而M0由于陷入系统调用而进被阻塞,M1接替M0的工作,只要P不空闲,就可以保证充分利用CPU。

M1的来源有可能是M的缓存池,也可能是新建的。当G0系统调用结束后,跟据M0是否能获取到P,将会将G0做不同的处理:

工作量窃取

多个P中维护的G队列有可能是不均衡的,比如下图:

2019-10-31T08:43:00.png

竖线左侧中右边的P已经将G全部执行完,然后去查询全局队列,全局队列中也没有G,而另一个M中除了正在运行的G外,队列中还有3个G待运行。此时,空闲的P会将其他P中的G偷取一部分过来,一般每次偷取一半。偷取完如右图所示。

抢占式调度

sysmon 会检查长时间运行的 G,将其中断并重新放入调度。中断的原理是 sysmon 通过信号量通知 G 的 M,往 G 的 PC 中插入特定指令,G 执行该指令后将自己推入全局队列重新调度。

性能

一般来讲,程序运行时就将 GOMAXPROCS 大小设置为 CPU 核数,可让 Go 程序充分利用 CPU。 在某些 IO 密集型的应用里,这个值可能并不意味着性能最好。 理论上当某个 Goroutine 进入系统调用时,会有一个新的 M 被启用或创建,继续占满 CPU。 但由于 Go 调度器检测到M被阻塞是有一定延迟的,也即旧的 M 被阻塞和新的 M 得到运行之间是有一定间隔的,所以在 IO 密集型应用中不妨把 GOMAXPROCS 设置的大一些,或许会有好的效果。

总结

参考

Responses