开始
- go程序启动,主机上定义的每一个虚拟内核都会为它分配一个逻辑处理器§,每个线程都会分配一个逻辑处理器
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Println(runtime.NumCPU()) // MacbookPro 2019 4核心8线程,返回结果8
}
- 会为每一个逻辑处理器(P)分配一个M(os线程)
- 线程是OS来处理的,并且OS还负责把线程放置到一个core上去执行
- 每个线程单独连到一个P上
- Go调度器运行在内核之上的用户空间(user space),Go 调度器中有两种不同的运行队列
- 全局运行队列(GRQ):负责管理没有分配到P的Goroutines
- 本地运行队列(LRQ):每个P会分配一个LRQ去处理P的上下文要执行的Goroutines 。这些Goroutines会在绑定到P的M上进行上下文的切换
Goroutine三种状态
- 等待中
- 可执行
- 执行中
Go程序中有4种类型的事件,允许调度器去做出调度决策
- go关键字:一旦一个新的Goroutine创建好,调度器便有机会去做出调度决定
- 垃圾回收
- 系统调用:如果一个Goroutine进行系统调用导致了M的阻塞,调度器有时候会用一个新的Goroutine从M上替换下这个Goroutine。但是有时候会需要一个新的M去执行挂在P队列上的Goroutine
- 同步处理:atomic、mutex或者是channel操作的调用导致了Goroutine的阻塞,调度器会切换一个新的Goroutine去执行
具体PMG模型过程可以看这个文章,更加详细Golang调度
- go程序启动时会首先创建一个特殊的内核线程sysmon
- 调度模型,go语言当前的实现是N:M。即一定数量的用户线程映射到一定数量的OS线程上,这里的用户线程在go中指的就是goroutine。go语言的调度模型需要弄清楚三个概念:M、P和G
- M:OS线程
- P:上下文可以理解为待执行goroutine队列
- G:Goroutine
触发PMG goroutine切换
- goroutine主动调用runtime.goshed()方法让出M
- goroutine调用runtime·park()方法,进入waiting状态
- goroutine等待慢系统调用
为了使该P上挂着的其它G也能得到执行的机会,需要将这些goroutine转到另一个OS线程上去。具体的做法是:首先将该P设置为syscall状态,然后该线程进入系统调用阻塞等待。之前提到过的sysmom线程会定期扫描所有的P,发现一个P处于了syscall的状态,就将M和P分离(实际上只有当 Syscall 执行时间超出某个阈值时,才会将 M 与 P 分离)。RUNTIME会再分配一个M和这个P绑定,从而继续执行队列中的其它G。而当之前阻塞的M从系统调用中返回后,会将该goroutine放入全局等待队列中,自己则sleep去