18

[译]Go:Goroutine, OS线程 以及 CPU管理

 4 years ago
source link: https://studygolang.com/articles/24986
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.

原文https://medium.com/a-journey-with-go/go-goroutine-os-thread-and-cpu-management-2f5a5eaf518a

qURB3mB.png!web

操作系统的线程创建以及切换是需要开销的,会影响程序的性能。Go致力于尽可能地从内核中获取优势,所以从最开始的时候设计就考虑到了并发性。

M,P,G 编排

为了解决这个问题,Go有他自己的调度者,负责在线程上分配goroutines。这个协调者由3个概念组成,如下:

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[...].
    处理者,负责执行Go代码, 每个M必须有一个关联的P去执行Go代码
复制代码

三者关系图如下:

vyMfQjn.png!web

每一个goruntine(G)运行在操作系统线程(M)上并分配一个逻辑CPU(P)。我们用一个简单的例子来看看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首先会基于机器逻辑CPUs的数量来创建不同的P,并将他们储存成一个空闲的P的列表

iuAbmuE.png!web

然后,当新的goroutine或者goroutine准备运行的时候会唤醒一个空闲的P,这个P会创建一个关联到操作系统线程的M

UzuQnqB.png!web

然而,当P,M不工作的,假如,没有goruntine在等待被执行的时候,就会返回一个系统调用syscall,或者甚至被垃圾回收强制停止,放回空闲P/M链表中。

Jjeq6r7.png!web

在程序运行时候,Go已经创建了一些OS线程以及关联上M。在我们的例子中,第一个负责打印 hello 的goroutine会使用主goroutine, 而第二个goroutine从空闲列表中获取一个P和M

YZFB7jm.png!web

现在我们已经对goroutines以及线程管理有一个大概了解了,让我们看看在什么时候Go会出现M数量比P多的情况以及goroutines是如何管理这种系统调用的。

系统调用 System calls

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
}
复制代码

下面是图片演示整个执行过程

v26NBvb.png!web

P0 现在在空闲列表中处于可被使用状态。一旦系统调用退出时,Go遵循下面的规制直到其中一个条件满足

P0

然而,Go同样需要处理当资源还没准备好的情况,例如HTTP请求这种非阻塞I/O。在这种情况下,第一个系统调用,同样会遵循上述规制但是不会成功,因为资源还没有准备好,这时会强迫Go使用network poller以及暂停goroutine。如下例子:

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

当第一个系统调用执行完成并明确地说资源还没准备好的时候,goroutine会暂停直到network poller通知其说资源已经准备好了。在这种情况下,线程M是不会被阻塞的。

qQR3uyn.png!web

当Go协调程序重新查找待完成工作时,goroutine会被重新执行一次。这个协调者在成功获取一个他所等待的消息以后,会问network poller是否有goroutine在等待运行。

2iqmUbU.png!web

如果有多于一个goroutine准备好的时候,其余的goroutine会进入全局的可执行队列中等待被执行。

OS线程的限制 Restriction in term of OS threads

当系统调用时,Go不会限制可以阻塞的OS线程的数量,官方解释:

GOMAXPROCS变量限制了可以同时执行用户级Go代码的操作系统线程的数量。 对于代表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()
}
复制代码

下面是跟踪工具里面展示的线程数量

2MniuyN.png!web

由于Go将线程的使用进行了优化,当goroutines被阻塞时候可以被重新利用,也就解释了为什么这个数与循环数并不匹配。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK