创建系统线程以及在系统线程间切换,会对程序的内存和性能造成较大的开销。
Go 的目标是尽量利用 CPU 多核资源。设计之初就考虑了高并发性。
M,P,G 模型
为了达到这个目标,Go
拥有一个将协程调度到系统线程执行的调度器。
这个调度器定义了三个核心概念,在Go
源码中是这样解释的:
M
- worker thread, or machine. 工作线程P
- processor(逻辑处理器), 执行 Go 代码时所必须的上下文环境。G
- goroutinue 协程
M
必须有一个相关联的P
才能执行Go
代码。
以下是 P,M,G
模型的示意图:
每个协程(
G
)在一个分配给逻辑处理器(P
)的系统线程(M
)上运行。
example
func main(){
var wg sync.WaitGroup
wg.add(2)
go func(){
fmt.Println("hello")
wg.Done()
}()
go func(){
fmt.Println("world")
wg.Done()
}()
wg.Wait()
}
execute stream
首先,
Go
会根据当前机器的逻辑CPU
个数来创建相应数量的P
,并将它们存放在一张空闲P
列表中
然后,新创建并等待被运行的 goroutine 协程会唤醒一个
P
来执行这个任务,这个P
会创建一个和系统内核级线程相关联的用户级线程M
和
P
一样,如果一个M
没有工作可做了,该M
会被放入空闲M
链表中
在程序启动时,
Go
会预先创建一些内核级线程
以及相关联的用户级线程M
。
在上面的小例子中,第一个打印hello
的协程会使用主协程,而第二个打印world
的协程会从空闲列表中获取到一个M
和一个P
以上,我们有了一张管理协程和内核线程的全局图,让我们进一步看看 Go
在什么情况下会使用更多的 M
和 P
,以及系统调用时协程是如何被管理的。
系统调用
Go
对系统调用做了优化,具体做法是在运行时对系统调用做了封装(不管系统调用是否会造成阻塞)。
该部分封装代码会自动将上下文环境 P
与用户线程 M
解除绑定,使得另一个用户线程 M
可以在这个 P
上运行。
让我们来看一个读取文件的例子:
func main(){
buf:=make([]byte,0,2)
fd,_ := os.Open("number.txt")
fd.Read(buf)
defer fd.Close()
fmt.Printlb(string(buf))
}
以下是打开文件的流程:
G
执行系统调用,P0
被放入空闲列表中,可被使用。
当系统调用结束之后,Go
顺序执行如下流程直到其中一条规则被满足:
- 试图获取同一个
P
,在我们上面的例子就是P0
,如果获取到,则恢复执行。 - 试图在空闲列表中获取一个
P
,如果获取到,则恢复执行。 - 将
G
(协程)放入全局队列中,将相关的M
放入空闲列表中。
Go
使用非阻塞 I/O
模式,对资源还没有就绪的情况也做了处理,比如说 http
请求。
这种情况下首先也遵循上面所说的系统调用的流程,之后如果底层的系统调用由于资源没有就绪而返回失败时,Go
会强制使用 network poller
,并且将该协程挂起。
以下是例子:
func main(){
http.Get("https://httpstat.us/200")
}
- 当底层的系统调用返回并且显式表示资源没有就绪时,协程将被挂起,直到
network poller
通知它资源已经就绪。- 这种情况下,线程
M
不会被阻塞,而是被调度去执行其它G
- 当
Go
调度器重新调度时,之前的那个G
(协程)将被重新运行。- 调度器会询问
network poller
是否存在之前在等待资源并且现在资源已经就绪的G
协程。- 如果有多个
G
(协程)就绪了,其它的G
(协程)会被放入全局等待执行队列中,稍后会被调度执行。
关于系统线程数的限制
当使用了系统调用时,Go
并不限制这些可能被阻塞的系统线程的数量,以下是Go
代码中的注释说明:
GOMAXPROCS
变量限制的是用户层面Go
代码的系统线程数量。
对于可能造成阻塞的系统调用的线程数是不做限制的;它们不计算在GOMAXPROCS
限制之中。
以下是一个例子
func main(){
var wg sync.WaitGroup
for i:=0;i<100;i++ {
wg.Add(1)
go func(){
http.Get(`https://httpstat.us/200?sleep=10000`)
wg.Done()
}()
}
wg.Wait()
}
值得一提,由于
Go
可以复用内核线程,所以工具查看到的线程数要小于例子中for
循环的次数。
以下是使用tracing
工具,查看程序创建的线程数量:
100
个协程被映射到了2
个线程上