30

[译] Go语言的协程,系统线程以及CPU管理

 4 years ago
source link: http://www.pengrl.com/p/29953/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Rr63Ibn.png!web

本文基于Go 1.13

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

M,P,G 模型

为了达到这个目标, Go 拥有一个将协程调度到系统线程执行的调度器。这个调度器定义了三个核心概念,在 Go 源码中是这样解释的:

G - goroutinue. 协程

M - worker thread, or machine. 工作线程

P - processor, 执行Go代码时所必须的一种资源。

M必须有一个相关联的P才能执行Go代码。

以下是 PMG 模型的示意图:

7JJ7n2Y.png!web

每个协程( G )在一个分配给逻辑 processorP )的系统线程( M )上运行。来看一个小例子:

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 列表中:

aQvEBj6.png!web

然后,新创建并等待被运行的协程会唤醒一个 P 来执行这个任务,这个 P 会创建一个和系统线程相关联的 M

z2iqQjQ.png!web

P 一样,如果一个 M 没有工作可做了,该 M 会被放入空闲 M 链表中:

jUJRRjm.png!web

在程序启动时, Go 会预先创建一些系统线程以及相关联的 M 。在上面的小例子中,第一个打印 hello 的协程会使用主协程,而第二个打印 world 的协程会从空闲列表中获取到一个 M 和一个 P

MZBfyyV.png!web

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

系统调用

Go 对系统调用做了优化,具体做法是在运行时对系统调用做了封装(不管系统调用是否会造成阻塞)。该部分封装代码会自动将 P 与线程 M 解除绑定,使得另一个线程 M 可以在这个 P 上运行。让我们来看一个读取文件的例子:

func main() {
   buf := make([]byte, 0, 2)

   fd, _ := os.Open("number.txt")
   fd.Read(buf)
   fd.Close()

   println(string(buf)) // 42
}

以下是打开文件的流程:

B7FzIjf.png!web

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

  • 试图获取同一个 P ,在我们上面的例子就是 P0 ,如果获取到,则恢复执行
  • 试图在空闲列表中获取一个 P ,如果获取到,则恢复执行
  • 将协程放入全局队列中,将相关的 M 放入空闲列表中

并且, Go 使用非阻塞 I/O 模式,对资源还没有就绪的情况也做了处理,比如说 http 请求。这种情况下首先也遵循上面所说的系统调用的流程,之后如果底层的系统调用由于资源没有就绪而返回失败时,Go会强制使用 network poller ,并且将该协程挂起。以下是例子:

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

当底层的系统调用返回并且显式表示资源没有就绪时,协程将被挂起,直到 network poller 通知它资源已经就绪。这种情况下,线程 M 不会被阻塞:

QZn2iyA.png!web

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

URJvaii.png!web

如果有多个协程就绪了,其它的协程会被放入全局等待执行队列中,稍后会被调度执行。

关于系统线程数的限制

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

以下是使用 tracing 工具,查看程序创建的线程数量:

RfUJr2F.png!web

值得一提,由于 Go 可以复用系统线程,所以工具查看到的线程数要小于例子中 for 循环的次数。

英文原文地址: Go: Goroutine, OS Thread and CPU Management

原文链接: https://pengrl.com/p/29953/

原文出处: yoko blog ( https://pengrl.com )

原文作者:yoko

版权声明:本文欢迎任何形式转载,转载时完整保留本声明信息(包含原文链接、原文出处、原文作者、版权声明)即可。本文后续所有修改都会第一时间在原始地址更新。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK