go GMP模型
GMP原理解析
操作系统的线程会被操作系统内核调度时会挂起当前执行的线程并将它的寄存器内容保存到内存中,选出下一次要执行的线程并从内存中恢复该线程的寄存器信息,然后恢复执行该线程的现场并开始执行线程。从一个线程切换到另一个线程需要完整的上下文切换。因为可能需要多次内存访问,索引这个切换上下文的操作开销较大,会增加运行的cpu
周期。
区别于操作系统内核调度操作系统线程,goroutine
的调度是Go语言运行时(runtime
)层面的实现,是完全由 Go
语言本身实现的一套调度系统——go scheduler
。它的作用是按照一定的规则将所有的 goroutine
调度到操作系统线程上执行。
在经历数个版本的迭代之后,目前 Go 语言的调度器采用的是 GPM
调度模型。
其中:
- G:表示
goroutine
,每执行一次go f()
就创建一个 G,包含要执行的函数和上下文信息。 - 全局队列(
Global Queue
):存放等待运行的 G。 - P:表示
goroutine
执行所需的资源,最多有GOMAXPROCS
个。 - P 的本地队列:同全局队列类似,存放的也是等待运行的G,存的数量有限,不超过256个。新建 G 时,G 优先加入到 P 的本地队列,如果本地队列满了会批量移动部分 G 到全局队列。
- M:线程想运行任务就得获取 P,从 P 的本地队列获取 G,当 P 的本地队列为空时,M 也会尝试从全局队列或其他 P 的本地队列获取 G。M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去。
Goroutine
调度器和操作系统调度器是通过 M 结合起来的,每个 M 都代表了1个内核线程,操作系统调度器负责把内核线程分配到 CPU 的核上执行。
单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的, goroutine
则是由Go运行时(runtime)自己的调度器调度的,完全是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc
函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine
均分在物理线程上, 再加上本身 goroutine
的超轻量级,以上种种特性保证了 goroutine
调度方面的性能。
GOMAXPROCS
Go运行时的调度器使用GOMAXPROCS
参数来确定需要使用多少个 OS 线程来同时执行 Go 代码。默认值是机器上的 CPU 核心数。例如在一个 8 核心的机器上,GOMAXPROCS
默认为 8。Go语言中可以通过runtime.GOMAXPROCS
函数设置当前程序并发时占用的 CPU逻辑核心数。(Go1.5
版本之前,默认使用的是单核心执行。Go1.5
版本之后,默认使用全部的CPU 逻辑核心数。)
func main() {
for i := 0; i < 50; i++ {
go func() {
fmt.Println(i)
}()
}
time.Sleep(1*time.Second)
// go 协程被调度执行,多以打印的i,不确定是多少
}