Go调度器: M,P和G

简介:

这是另一篇关于Go调度器(scheduler)的文章。 原文: GO SCHEDULER: MS, PS & GS by Uber工程师 Povilas。

网上已经有很多关于Go调度器的文章了, 比如 Golang调度器源码分析 ,多看一些,可以加深记忆,也可以对比查看文章中是否有不准确的地方,更全面的了解Go的调度器。

我决定深入了解Go的内部机制, 因为很长时间没人写关于Go scheduler的文章了, 我觉得这是一个很有趣的知识点,所以让我们开始吧。

基础知识

Go的运行时管理着调度、垃圾回收以及goroutine的运行环境。本文只关注于调度器。

运行时负责运行goroutine并把它们影射到操作系统的线程上。goroutine比线程还轻量, 启动的时候花费很少。每个goroutine都是由一个 G 结构来表示,

这个结构体的字段用来跟踪此goroutine的栈(stack)和状态,所以你可以认为 G = goroutine 。

运行时管理着 G 并把它们映射到 Logical Processor (称之为 P ). P 可以看作是一个抽象的资源或者一个上下文,它需要获取以便操作系统线程(称之为 M )可以运行 G 。

通过 runtime.GOMAXPROCS (numLogicalProcessors) 可以控制多少 P 可以获取。如果你需要调整这个参数(大部分情况下你无需调整), 只设置一次, 因为它需要 STW gc pause。

本质上,操作系统运行线程,线程运行你的代码。Go的技巧是编译器会在Go运行时的一些地方插入系统调用, (比如通过channel发送值,调用runtime包等),所以Go可以通知调度器执行特定的操作。

Go调度器: M,P和G

上图的理解来自 Analysis of the Go runtime scheduler

M、P 和 G 之间的交互

M、 P 和 G 之间的交互有点复杂。看看下面这张来自 Gao Chao的 go runtime scheduler 幻灯片中的一张图:

Go调度器: M,P和G

可以看到,Go运行时存在两种类型的queue: 一种是一个全局的queue(在 schedt结构体中 ,很少用到), 一种是每个 P 都维护自己的 G 的queue。

为了运行goroutine, M 需要持有上下文 P 。 M 会从 P 的queue弹出一个goroutine并执行。

当你创建一个新的goroutine的时候( go func() 方法),它会被放入 P 的queue。当然还有一个 work-stealing 调度算法,当 M 执行了一些 G 后,如果它的queue为空,它会随机的选择另外一个 P ,从它的queue中取走一半的 G 到自己的queue中执行。(偷!)

当你的goroutine执行阻塞的系统调用的时候(syscall),阻塞的系统调用会中断(intercepted),如果当前有一些 G 在执行,运行时会把这个线程从 P 中摘除(detach),然后再创建一个新的操作系统的线程(如果没有空闲的线程可用的话)来服务于这个 P 。

当系统调用继续的时候,这个goroutine被放入到本地运行queue,线程会 park 它自己(休眠), 加入到空闲线程中。

如果一个goroutine执行网络调用,运行时会做类似的动作。调用会被中断,但是由于Go使用集成的network poller,它有自己的线程,所以还给它。

Go运行时会在下面的goroutine被阻塞的情况下运行另外一个goroutine:

  • blocking syscall (for example opening a file),
  • network input,
  • channel operations,
  • primitives in the sync package.

调度器跟踪调试

Go可以跟踪运行时的调度器,这是通过 GODEBUG 环境变量实现的:


 
 
  1. $ GODEBUG=scheddetail=1,schedtrace=1000 ./program 

下面是输出的例子:


 
 
  1. SCHED 0ms: gomaxprocs=8 idleprocs=7 threads=2 spinningthreads=0 idlethreads=0 runqueue=0 gcwaiting=0 nmidlelocked=0 stopwait=0 sysmonwait=0 
  2.   P0: status=1 schedtick=0 syscalltick=0 m=0 runqsize=0 gfreecnt=0 
  3.   P1: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0 
  4.   P2: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0 
  5.   P3: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0 
  6.   P4: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0 
  7.   P5: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0 
  8.   P6: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0 
  9.   P7: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0 
  10.   M1: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 helpgc=0 spinning=false blocked=false lockedg=-1 
  11.   M0: p=0 curg=1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 helpgc=0 spinning=false blocked=false lockedg=1 
  12.   G1: status=8() m=0 lockedm=0 

注意输出使用了 G 、 M 和 P 的概念以及她们的状态, 比如 P 的queue的大小。 如果你不想关心这些细节,你可以使用:


 
 
  1. $ GODEBUG=schedtrace=1000 ./program 

William Kennedy写了一篇很好的 文章 , 解释了这些细节。

当然,还有一个go自己的工具 go tool trace , 它有一个UI, 允许你查看你的程序和运行时的状况。你可以阅读这篇文章: Pusher 。


作者:佚名

来源:51CTO

相关文章
|
6月前
|
缓存 负载均衡 Java
Go语言调度器机制详解
【2月更文挑战第16天】Go语言以其强大的并发编程能力而闻名,这背后离不开其高效的调度器机制。本文将对Go语言的调度器机制进行详细的解析,包括调度器的设计原理、核心组件、调度策略以及优化技巧等方面,帮助读者深入理解Go语言并发编程的底层原理,更好地发挥Go语言并发编程的优势。
|
1月前
|
安全 编译器 Go
Go runtime 调度器精讲(十):异步抢占
Go runtime 调度器精讲(十):异步抢占
|
1月前
|
监控 Go 定位技术
Go runtime 调度器精讲(八):运行时间过长的抢占
Go runtime 调度器精讲(八):运行时间过长的抢占
|
6月前
|
资源调度 监控 Go
|
负载均衡 Go 调度
go的调度器
go的调度器是完成协程的组件。
62 0
|
存储 缓存 算法
Go并发调度进阶-GMP和调度器的主要结构,只有接触到底层你才更有底气
Go并发调度进阶-GMP和调度器的主要结构,只有接触到底层你才更有底气
Go并发调度-调度器设计理念从何而来?为何如此高效?
Go并发调度-调度器设计理念从何而来?为何如此高效?
|
存储 缓存 监控
Go语言的GPM调度器是什么?
云栖号资讯:【点击查看更多行业资讯】在这里您可以找到不同行业的第一手的上云资讯,还在等什么,快来! 相信很多人都听说过Go语言天然支持高并发,原因是内部有协程(goroutine)加持,可以在一个进程中启动成千上万个协程。
Go语言的GPM调度器是什么?
|
3天前
|
存储 JSON 监控
Viper,一个Go语言配置管理神器!
Viper 是一个功能强大的 Go 语言配置管理库,支持从多种来源读取配置,包括文件、环境变量、远程配置中心等。本文详细介绍了 Viper 的核心特性和使用方法,包括从本地 YAML 文件和 Consul 远程配置中心读取配置的示例。Viper 的多来源配置、动态配置和轻松集成特性使其成为管理复杂应用配置的理想选择。
12 2
|
5天前
|
程序员 Go
go语言中的控制结构
【11月更文挑战第3天】
79 58