Go: Goroutine, 系统线程和 CPU 管理
ℹ️ 本文基于Go1.13。
创建系统线程或在系统线程间切换,会对程序的内存和性能造成较大的开销。Go旨在尽量多的利用多核资源并在设计之初就考虑了并发性。
M, P, G 编排
为了解决这个问题,Go有自己的调度程序,可以在线程上分配goroutine。 该调度程序定义了三个主要概念,如代码本身所述:
这是P,M,G模型的示意图:
每个goroutine(G)在分配给逻辑CPU(P)的系统线程(M)上运行。 让我们举一个简单的例子,看看Go如何管理它们:
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分离,并允许另一个线程在其上运行。让我们以一个文件读取为例:
这是打开文件工作流:
现在,P0
被放入空闲列表中,可被使用。当系统调用结束之后,Go顺序执行如下流程直到其中一条规则被满足:
试图获取同一个P,在我们上面的例子就是P
0
,如果获取到,则恢复执行试图在空闲列表中获取一个P,如果获取到,则恢复执行
将协程放入全局队列中,将相关的M放入空闲列表中
并且,Go对非阻塞 I/O (例如http调用)这种资源尚未就绪的情况也做了处理。在这种情况时,系统调用会遵循以上的工作流,但是会因为资源尚未就绪导致失败,然后Go会强制使用network poller来停放goroutine。这是一个例子:
一旦系统调用完成并明确表示资源尚未就绪,goroutine将停放,直到network poller通知它资源现在已准备就绪。在这种情况下,线程M不会被阻塞:
当Go调度器重新调度时,之前的那个goroutine将被重新运行。调度器会询问network poller是否存在之前在等待资源并且现在资源已经就绪的goroutine:
如果有超过一个goroutine就绪,则另外的goroutine将被放到全局队列中等待后续调度。
对系统线程数的限制
当使用了系统调用时,Go并不限制这些可能被阻塞的系统线程的数量,以下是Go代码中的注释说明:
GOMAXPROCS 变量限制的是用户层面 Go 代码的系统线程数量。对于可能造成阻塞的系统调用的线程数是不做限制的;它们不计算在 GOMAXPROCS 限制之中。
这里是一个相关的例子:
这是通过trace工具得到的线程数:
因为Go可以复用系统线程,所以工具查看到的线程数要小于例子中循环的次数。
编译自:https://medium.com/a-journey-with-go/go-goroutine-os-thread-and-cpu-management-2f5a5eaf518a
博客地址:https://www.chenjie.info/2566
本文首发于我的公众号:
版权声明: 本文为 InfoQ 作者【陈思敏捷】的原创文章。
原文链接:【http://xie.infoq.cn/article/815ddf05adb93bd819038bc56】。文章转载请联系作者。
评论