32|并发:聊聊Goroutine调度器的原理
该思维导图由 AI 生成,仅供参考
Goroutine 调度器
- 深入了解
- 翻译
- 解释
- 总结
Go语言中的Goroutine调度器经历了多次演化和优化,从最初的G-M模型到G-P-M模型,再到支持抢占式调度和基于信号的异步抢占。Goroutine调度器的目标是公平合理地将各个G调度到P上“运行”,并通过抢占调度、channel阻塞、网络I/O和系统调用等方式实现G的调度。Goroutine调度器的演进体现了Go语言并发模型的不断优化与改进,为读者提供了深入理解Go语言并发模型的机会。文章通过对G、P、M的定义和G的抢占调度机制的解释,帮助读者理解了Goroutine调度器的工作原理。同时,通过思考题引发读者思考,加深对Goroutine调度原理的理解。整体而言,本文详细介绍了Goroutine调度器的演化历程和工作原理,为读者提供了全面的技术内容。
《Tony Bai · Go 语言第一课》,新⼈⾸单¥59
全部留言(25)
- 最新
- 精选
- Geek_cca544go1.13的话加上runtime.GOMAXPROCS(1) main goroutine在创建 deadloop goroutine 之后就无法继续得到调度 但如果是go1.14之后的话即使加上runtime.GOMAXPROCS(1) main goroutine在创建 deadloop goroutine 之后还是可以得到调度,应该是因为增加了对非协作的抢占式调度的支持
作者回复: ✅
2022-01-1148 - lesserror大白老师这篇算是让我重新对Go的G、P、M模型有了一个新的认识。感谢。不过还是有几处疑惑的地方: 1. 怎么理解文中的:“集中状态存储”和“数据局部性较差”,能再进一步解释一下么? 2. 编译器在每个函数或方法的入口处加上了一段额外的代码 (runtime.morestack_noctxt),括号中的:runtime.morestack_noctxt 这是一个文件么? 3. 怎么理解“协作式”、“非协作式”呢?看了文章还是没太明白。 4. 关于挂起,百度的说法大概是:“暂时被淘汰出内存的进程。”,这里该怎么理解呢? 5. 为什么 M有时必须要与 G 一起挂起?M 不是可以不保存 G的状态的吗?M不能直接去绑定别的p吗?为什么要频繁的挂起呢? PS:老师的文档链接好评,最原始的出处标准的很明确。
作者回复: 1.按照Dmitry Vyukov原文的意思: 集中状态(centralized state),我理解就是一把全局锁要保护的数据太多。这样无论访问哪个数据,都要锁这把全局锁。数据局部性差是因为每个m都会缓存它执行的代码或数据,但是如果在m之间频繁传递goroutine,那么这种局部缓存的意义就没有了。无法实现局部缓存带来的性能提升。 2. runtime.morestack_noctxt 是一个函数。 3. 协作式:大家都按事先定义好的规则来,比如:一个goroutine执行完后,退出,让出p,然后下一个goroutine被调度到p上运行。这样做的缺点就在于 是否让出p的决定权在groutine自身。一旦某个g不主动让出p或执行时间较长,那么后面的goroutine只能等着,没有方法让前者让出p,导致延迟甚至饿死。而非协作: 就是由runtime来决定一个goroutine运行多长时间,如果你不主动让出,对不起,我有手段可以抢占你,把你踢出去,让后面的goroutine进来运行。 4. 挂起这里你可以理解为暂停运行,等待下一次调度。 5. 当进行一些慢系统调用的时候,比如常规文件io,执行系统调用的m就要和g一起挂起,这是os的要求,不是go runtime的要求。毕竟真正执行代码的还是m。
2022-01-1331 - ivhong谢谢大白老师,能把这么晦涩的原理讲的这么清楚。我反复看了很多遍,做了如下总结,不知道是不是合理,希望老师闲暇之余给予指正,🙏。 在文中提及的GPM,以及GPM之前的相互配合,完成了go的并发功能。 G:可以看作关键字go 后面跟着的可执行代码块(即goroutine),当然包含这个代码块的一些本身的信息(比如栈信息、状态等等一些必要的属性)。另外存在一个G的全局队列,只要是需要执行的 goroutine 都会被放倒这个全局队列里。 P:可以看作逻辑上的“任务处理器”,go有多个这个处理器,具体的数量由runtime.GOMAXPROCS(1)指定,它有下面的指责: 1. 它有自己G队列。当他发现自己的队列为空时,可以去全局G队列里获取等待执行的goroutine,甚至可以去其他的P的队列里抢用goroutine。他把拿过来的goroutine放到自己的队列里 2. 他可以找到一个空闲的M与自己绑定,用来运行自己队列中的goroutine,如果没有空闲的M,则创建一个M来绑定 3. 被P绑定的M,可以自己主动的与P解绑,当P发现自己的M被解绑,就执行2 4. 如果自己队列中没有goroutine,也无法从“外面”获取goroutine,则与M解绑(解绑M时,是按什么逻辑选择挂起M或者释放M呢?) M:物理的处理器,具体执行goroutine的系统线程,所有goroutine都是在M中执行的,它被P创建,与P绑定后可执行P队列中的goroutine,在执行goroutine会处理3中情况保证并发是顺利的(不会发生“饿死”,资源分配不平等的情况) 1. 当G长时间运行时,可以被Go暂停执行而被移出M(是不是放到全局G队列呢?),等待下次运行(即抢占式调度)。 2. 如果G发生了channel 等待或者 网络I/O请求,则把G放到某个等待队列中,M继续执行下一goroutine,当G等待的结果返回时,会唤醒“G”,并把它放入到全局G的队列中,等待P的获取(这里不知道理解的对不对?)。 3. 如果G产生了系统调用,则M与P解绑,然后M和它正执行的G被操作系统“挂起”等待操作系统的中断返回(对于操作系统而言,M和G是一回事;而对于GO来说,G只有能在M中运行,只有运行才触发发系统调用)。
作者回复: 👍
2022-03-1518 - 麦芒极客老师您好,G遇到网络IO阻塞时,真正的线程即M不应该也阻塞吗?
作者回复: 好问题!不过当网络I/O阻塞时,M真不会阻塞。 因为runtime层实现了netpoller,netpoller是基于os提供的I/O多路复用模型实现的(比如:linux上的epoll),这允许一个线程处理多个socket。这就使得当某个goroutine的socket发送i/o阻塞时,仅会让goroutine变为阻塞状态,runtime会将对应的socket与goroutine加入到netpoller的监听中,然后M继续执行其他goroutine的任务。等netpoller监视到之前的socket可读/可写时,再把对应的goroutine唤醒继续执行网络i/o。
2022-05-2217 - bearlu老师,想学习goroutine调度器,演进的关键版本,依次是go的什么发行版?还有什么相关资料书籍?谢谢老师
作者回复: 欧长坤老师维护的这个go history中有关调度器的演化可以参考:https://golang.design/history/#scheduler
2022-01-1026 - Mr_D请问G的可重用具体指什么呢?是针对已经运行结束的G留下的内存空间,进行相关数据的重填么
作者回复: 对,goroutine退出后,runtime层的G对象会被放入一个Free G对象的池子中,后续有新Goroutine创建时,优先从池子里get一个。可以看Go源码:runtime/proc.go中的代码。
2022-08-11归属地:辽宁3 - 奕思考题: 是不是不管怎么处理,main goroutine 都会被调度?
作者回复: 试试将P的数量置为1.
2022-01-1023 - 微微超级丹💫请问常规文件是不支持监听的(pollable)是什么意思呀?
作者回复: 就是说对常规文件的读写操作(因为是系统调用)依然会阻塞M。
2022-09-20归属地:北京2 - 路边的猪第一种,因为网络io导致阻塞的处理方式这里。我想问,网络io势必会引起系统调用,比如最基础的建立tcp连接这些,那这块儿是咋区分系统调用和网络io的呢?
作者回复: 由于go运行时netpoller的存在,我们很难精确区分。这种“网络io”会被runtime转换为goroutine的调度等,不一定真的实施网络io。
2022-06-052 - 多选参数有几个问题想问老师: 1. 文章中提到的 Go 运行时这块更多是指哪些内容?Go 调度器应该也是 Go 运行时中的部分内容吧?就是 Go 开源的源代码吗? 2. 有关 Go 运行时比较好的资料推荐吗?比如 Go 运行时的架构、Go 运行时是怎么和用户编写的代码合在一起的,因为我理解的 Go 运行时不像 JVM 那样,Go 是编译性的,也就是说会将用户编写的代码和 Go 运行时代码一起编译成二进制,然后运行是从 Go 运行时开始的,然后在从用户逻辑的代码开始。 老师有空的时候麻烦解答下,谢谢老师~
作者回复: 《Go语言第一课FAQ》中有这方面的解答,你可以先看看。 https://tonybai.com/go-course-faq
2022-01-292