[译]Go:Goroutine, OS线程 以及 CPU管理
野生程序元 · · 1530 次点击 · · 开始浏览原文https://medium.com/a-journey-with-go/go-goroutine-os-thread-and-cpu-management-2f5a5eaf518a
操作系统的线程创建以及切换是需要开销的,会影响程序的性能。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代码
复制代码三者关系图如下:
每一个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的列表
然后,当新的goroutine或者goroutine准备运行的时候会唤醒一个空闲的P,这个P会创建一个关联到操作系统线程的M
然而,当P,M不工作的,假如,没有goruntine在等待被执行的时候,就会返回一个系统调用syscall,或者甚至被垃圾回收强制停止,放回空闲P/M链表中。
在程序运行时候,Go已经创建了一些OS线程以及关联上M。在我们的例子中,第一个负责打印hello的goroutine会使用主goroutine, 而第二个goroutine从空闲列表中获取一个P和M
现在我们已经对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
}
复制代码下面是图片演示整个执行过程
P0现在在空闲列表中处于可被使用状态。一旦系统调用退出时,Go遵循下面的规制直到其中一个条件满足
- 尝试去获得一个一模一样的P,在我们的例子中就是
P0,然后恢复执行 - 尝试获取空闲列表中的P,然后恢复执行
- 将goroutine到全局队列中,然后将其关联的M放回到空闲列表中
然而,Go同样需要处理当资源还没准备好的情况,例如HTTP请求这种非阻塞I/O。在这种情况下,第一个系统调用,同样会遵循上述规制但是不会成功,因为资源还没有准备好,这时会强迫Go使用network poller以及暂停goroutine。如下例子:
func main() {
http.Get(`https://httpstat.us/200`)
}
复制代码当第一个系统调用执行完成并明确地说资源还没准备好的时候,goroutine会暂停直到network poller通知其说资源已经准备好了。在这种情况下,线程M是不会被阻塞的。
当Go协调程序重新查找待完成工作时,goroutine会被重新执行一次。这个协调者在成功获取一个他所等待的消息以后,会问network poller是否有goroutine在等待运行。
如果有多于一个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()
}
复制代码下面是跟踪工具里面展示的线程数量
由于Go将线程的使用进行了优化,当goroutines被阻塞时候可以被重新利用,也就解释了为什么这个数与循环数并不匹配。
有疑问加站长微信联系(非本文作者)
入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889
关注微信- 请尽量让自己的回复能够对别人有帮助
- 支持 Markdown 格式, **粗体**、~~删除线~~、
`单行代码` - 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
- 图片支持拖拽、截图粘贴等方式上传
收入到我管理的专栏 新建专栏
原文https://medium.com/a-journey-with-go/go-goroutine-os-thread-and-cpu-management-2f5a5eaf518a
操作系统的线程创建以及切换是需要开销的,会影响程序的性能。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代码
复制代码三者关系图如下:
每一个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的列表
然后,当新的goroutine或者goroutine准备运行的时候会唤醒一个空闲的P,这个P会创建一个关联到操作系统线程的M
然而,当P,M不工作的,假如,没有goruntine在等待被执行的时候,就会返回一个系统调用syscall,或者甚至被垃圾回收强制停止,放回空闲P/M链表中。
在程序运行时候,Go已经创建了一些OS线程以及关联上M。在我们的例子中,第一个负责打印hello的goroutine会使用主goroutine, 而第二个goroutine从空闲列表中获取一个P和M
现在我们已经对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
}
复制代码下面是图片演示整个执行过程
P0现在在空闲列表中处于可被使用状态。一旦系统调用退出时,Go遵循下面的规制直到其中一个条件满足
- 尝试去获得一个一模一样的P,在我们的例子中就是
P0,然后恢复执行 - 尝试获取空闲列表中的P,然后恢复执行
- 将goroutine到全局队列中,然后将其关联的M放回到空闲列表中
然而,Go同样需要处理当资源还没准备好的情况,例如HTTP请求这种非阻塞I/O。在这种情况下,第一个系统调用,同样会遵循上述规制但是不会成功,因为资源还没有准备好,这时会强迫Go使用network poller以及暂停goroutine。如下例子:
func main() {
http.Get(`https://httpstat.us/200`)
}
复制代码当第一个系统调用执行完成并明确地说资源还没准备好的时候,goroutine会暂停直到network poller通知其说资源已经准备好了。在这种情况下,线程M是不会被阻塞的。
当Go协调程序重新查找待完成工作时,goroutine会被重新执行一次。这个协调者在成功获取一个他所等待的消息以后,会问network poller是否有goroutine在等待运行。
如果有多于一个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()
}
复制代码下面是跟踪工具里面展示的线程数量
由于Go将线程的使用进行了优化,当goroutines被阻塞时候可以被重新利用,也就解释了为什么这个数与循环数并不匹配。