互联网上已经有很多关于go调度器的文章了,阅读这些文章可以帮助加深理解,并且通过对比,可以检查文章中是否存在错误或不准确之处,从而更全面地了解go的调度器。
我决定深入研究Go的内部机制,因为很长时间没有新的关于Go调度器的文章了,我认为这是个非常有趣的知识点,所以我们就开始吧。
Go的运行时管理着调度、垃圾回收以及goroutine的运行环境。本文仅关注调度器。
运行时负责运行goroutine并将它们映射到操作系统的线程上。goroutine比线程更轻量,启动时消耗的资源非常少。每个goroutine由一个G结构体表示,该结构体的字段用于跟踪此goroutine的栈和状态,所以可以认为G = goroutine。
运行时管理G并将它们映射到逻辑处理器(称为P)。P可以看作是一个抽象的资源或上下文,需要获取以便操作系统线程(称为M)可以运行G。
通过runtime.GOMAXPROCS(numLogicalProcessors)可以控制可获取的P数量。如果需要调整这个参数(大多数情况下不需要调整),只需设置一次,因为它需要STW gc暂停。
本质上,操作系统运行线程,线程运行你的代码。Go的技巧是编译器会在Go运行时的一些地方插入系统调用(例如通过通道发送值,调用runtime包等),这样Go可以通知调度器执行特定的操作。
上图的理解来自于Analysis of the Go runtime scheduler。
M、P和G之间的交互有点复杂。看看下面这张来自Gao Chao的go runtime scheduler幻灯片中的一张图:
可以看到,Go运行时存在两种类型的队列:一种是全局队列(在schedt结构体中,很少使用),另一种是每个P都维护自己的G队列。
为了运行goroutine,M需要持有上下文P。M会从P的队列中弹出一个goroutine并执行。
当你创建一个新的goroutine时(go func()方法),它会被放入P的队列。当然还有一个work-stealing调度算法,当M执行了一些G后,如果它的队列为空,它会随机选择另一个P,从它的队列中取走一半的G到自己的队列中执行。(偷取!)
当你的goroutine执行阻塞的系统调用时(syscall),阻塞的系统调用会被中断,如果当前有一些G在执行,运行时会将这个线程从P中移除(detach),然后再创建一个新的操作系统线程(如果没有空闲线程可用的话)来服务于这个P。
当系统调用继续时,这个goroutine会被放入到本地运行队列,线程会park自己(休眠),加入到空闲线程中。
如果一个goroutine执行网络调用,运行时会做类似的动作。调用会被中断,但由于Go使用集成的网络轮询器,它有自己的线程,所以还给它。
Go运行时会在下面的goroutine被阻塞的情况下运行另一个goroutine:
Go可以跟踪运行时的调度器,这是通过GODEBUG环境变量实现的:
运行命令:GODEBUG=scheddetail=1,schedtrace=1000 ./program
当然,还有一个Go自己的工具go tool trace,它有一个UI,允许你查看你的程序和运行时的状态。你可以阅读这篇文章:Pusher。
以上就是Go 调度器 M, P 和 G的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号