写点什么

Go: Goroutine, 系统线程和 CPU 管理

用户头像
陈思敏捷
关注
发布于: 2020 年 08 月 14 日
Go: Goroutine, 系统线程和CPU管理

ℹ️ 本文基于Go1.13。



创建系统线程或在系统线程间切换,会对程序的内存和性能造成较大的开销。Go旨在尽量多的利用多核资源并在设计之初就考虑了并发性。



M, P, G 编排

为了解决这个问题,Go有自己的调度程序,可以在线程上分配goroutine。 该调度程序定义了三个主要概念,如代码本身所述:

The main concepts are:
G - goroutine.
M - worker thread, or machine.
P - processor, a resource that is required to execute Go code.
M must have an associated P to execute Go code[...].

这是P,M,G模型的示意图:





每个goroutine(G)在分配给逻辑CPU(P)的系统线程(M)上运行。 让我们举一个简单的例子,看看Go如何管理它们:

func main() {
var wg sync.WaitGroup
wg.Add(2)
go func() {
println(`hello`)
wg.Done()
}()
go func() {
println(`world`)
wg.Done()
}()
wg.Wait()
}

Go将首先根据机器的逻辑CPU数量创建不同的P,并将它们存储在空闲P的列表中:





然后,准备运行的新goroutine将唤醒一个P来执行任务。此P将创建一个和系统线程相关联的M:





与P一样,没有工作的M将会进入空闲列表:





在程序启动过程中,Go已经创建了一些系统线程和关联的M。在我们的示例中,打印hello的第一个goroutine将使用主goroutine,而第二个goroutine将从这个空闲列表中获取M和P:





现在我们有了一张管理goroutine和系统线程的全局图,让我们进一步看看Go在什么情况下会使用更多的M和P,以及系统调用时goroutine是如何被管理的。



系统调用

Go通过在运行时包装系统调用来优化系统调用(无论它是否阻塞)。这个包装器将自动将P与线程M分离,并允许另一个线程在其上运行。让我们以一个文件读取为例:

func main() {
buf := make([]byte, 0, 2)
fd, _ := os.Open("number.txt")
fd.Read(buf)
fd.Close()
println(string(buf)) // 42
}

这是打开文件工作流:





现在,P0被放入空闲列表中,可被使用。当系统调用结束之后,Go顺序执行如下流程直到其中一条规则被满足:



  • 试图获取同一个P,在我们上面的例子就是P0,如果获取到,则恢复执行

  • 试图在空闲列表中获取一个P,如果获取到,则恢复执行

  • 将协程放入全局队列中,将相关的M放入空闲列表中



并且,Go对非阻塞 I/O (例如http调用)这种资源尚未就绪的情况也做了处理。在这种情况时,系统调用会遵循以上的工作流,但是会因为资源尚未就绪导致失败,然后Go会强制使用network poller来停放goroutine。这是一个例子:

func main() {
http.Get(`https://httpstat.us/200`)
}



一旦系统调用完成并明确表示资源尚未就绪,goroutine将停放,直到network poller通知它资源现在已准备就绪。在这种情况下,线程M不会被阻塞:





当Go调度器重新调度时,之前的那个goroutine将被重新运行。调度器会询问network poller是否存在之前在等待资源并且现在资源已经就绪的goroutine:





如果有超过一个goroutine就绪,则另外的goroutine将被放到全局队列中等待后续调度。



对系统线程数的限制

当使用了系统调用时,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()
}

这是通过trace工具得到的线程数:





因为Go可以复用系统线程,所以工具查看到的线程数要小于例子中循环的次数。



编译自:https://medium.com/a-journey-with-go/go-goroutine-os-thread-and-cpu-management-2f5a5eaf518a

博客地址:https://www.chenjie.info/2566



本文首发于我的公众号:



发布于: 2020 年 08 月 14 日阅读数: 85
用户头像

陈思敏捷

关注

多动脑不痴呆 2017.12.21 加入

gopher

评论

发布
暂无评论
Go: Goroutine, 系统线程和CPU管理