32|并发:聊聊Goroutine调度器的原理
32|并发:聊聊Goroutine调度器的原理
讲述:Tony Bai
时长17:15大小15.75M
Goroutine 调度器
Goroutine 调度器模型与演化过程
深入 G-P-M 模型
G、P 和 M
G 被抢占调度
小结
思考题
赞 25
提建议
精选留言(23)
- Geek_cca5442022-01-11go1.13的话加上runtime.GOMAXPROCS(1) main goroutine在创建 deadloop goroutine 之后就无法继续得到调度 但如果是go1.14之后的话即使加上runtime.GOMAXPROCS(1) main goroutine在创建 deadloop goroutine 之后还是可以得到调度,应该是因为增加了对非协作的抢占式调度的支持
作者回复: ✅
37 - lesserror2022-01-13大白老师这篇算是让我重新对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。
19 - ivhong2022-03-15谢谢大白老师,能把这么晦涩的原理讲的这么清楚。我反复看了很多遍,做了如下总结,不知道是不是合理,希望老师闲暇之余给予指正,🙏。 在文中提及的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中运行,只有运行才触发发系统调用)。展开
作者回复: 👍
12 - 麦芒极客2022-05-22老师您好,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。
8 - bearlu2022-01-10老师,想学习goroutine调度器,演进的关键版本,依次是go的什么发行版?还有什么相关资料书籍?谢谢老师
作者回复: 欧长坤老师维护的这个go history中有关调度器的演化可以参考:https://golang.design/history/#scheduler
共 2 条评论4 - return2022-01-11老师讲的太好了,干货慢慢,老师已经把流程和原理讲清楚了, 原理下的 更底层细节 得自己再研究学习。
作者回复: 👍
2 - DullBird2022-01-11小结上面一句:之前的那个挂起的 M 将再次进入挂起状态。 最后是不是运行状态吧?
作者回复: 文里有说,是在系统调用返回后,找不到idle的P的情况下,m会进入到idle m池中挂起。
1 - 一步2022-01-10思考题: 是不是不管怎么处理,main goroutine 都会被调度?
作者回复: 试试将P的数量置为1.
共 2 条评论1 - 李亮2022-10-21 来自北京抢占是什么意思?G被抢占是指本来停止运行G,换成另外一个G吗?
作者回复: 原先正在运行的G被强行终止,换另外一个G运行。
- Geek_1d26612022-10-18 来自北京1.用1.13是实现了抢占式调度 但是那个是通过函数埋点的 这里的不是函数 所以不会运行吧 2.使用函数调度应该可以吧
作者回复: 1. 可从多核角度考虑一下 2. 还是从可利用的调度资源角度考虑一下。
- piboye2022-10-02 来自江西gpm模型还是有性能问题吧? m应该要尽量绑核,阻塞调用,应该用其它线程去处理,尽量不要占用当前m。
- 微微超级丹💫2022-09-20 来自北京请问常规文件是不支持监听的(pollable)是什么意思呀?
作者回复: 就是说对常规文件的读写操作(因为是系统调用)依然会阻塞M。
- 柒2022-09-15 来自美国Goroutine 传递问题:M 经常在 M 之间传递“可运行”的 Goroutine?为啥会有传递呀? 就算是上下文切换,只会挂起一个G,M切换到执行另一个G,它不会从一个M传递到另一个M嘛。
作者回复: 首先这是P-M模型中的问题; 其次,可运行是指runnable,即该G在nextG或M的本地队列中(P-M模型),并不是正在M上运行的G; 最后,当在M上运行的G出现慢系统调用后,M和G挂起,其nextG和本地队列中的G可能就要被传递给其他M(已有的idle m或新建M)处理。
共 2 条评论 - 菠萝吹雪—Code2022-09-08 来自北京打卡
作者回复: 👍
- Mr_D2022-08-11 来自辽宁请问G的可重用具体指什么呢?是针对已经运行结束的G留下的内存空间,进行相关数据的重填么
作者回复: 对,goroutine退出后,runtime层的G对象会被放入一个Free G对象的池子中,后续有新Goroutine创建时,优先从池子里get一个。可以看Go源码:runtime/proc.go中的代码。
- 每天晒白牙2022-07-19思考题 1.在一个拥有多核处理器的主机上,使用 Go 1.13.x 版本运行这个示例代码,你在命令行终端上是否能看到“I got scheduled!”输出呢?也就是 main goroutine 在创建 deadloop goroutine 之后是否能继续得到调度呢? 答:不会得到调度,首先 Go 是在 1.2 版本增加的抢占式调度,1.13.x版本还不支持,因为deadloop goroutine 是死循环,所以这个G会永远占用分配的P和M 2.我们通过什么方法可以让上面示例中的 main goroutine,在创建 deadloop goroutine 之后无法继续得到调度? 两个必要条件 ①go的版本要在1.2之前,因为1.2之前不支持抢占式调度,可以确保G可以占用分配给它的P和M ②设置GOMAXPROCS=1展开
作者回复: “Go 是在 1.2 版本增加的抢占式调度,1.13.x版本还不支持” , 1.2版本?
- 路边的猪2022-06-05第一种,因为网络io导致阻塞的处理方式这里。我想问,网络io势必会引起系统调用,比如最基础的建立tcp连接这些,那这块儿是咋区分系统调用和网络io的呢?
作者回复: 由于go运行时netpoller的存在,我们很难精确区分。这种“网络io”会被runtime转换为goroutine的调度等,不一定真的实施网络io。
- pythonbug2022-04-28“如果一个 G 任务运行 10ms,...... 那么等到这个 G 下一次调用函数或方法时,...... 等待下一次被调度。” 老师好,有个问题,某个goroutine运行超过10ms了,为啥不直接抢占,等待下一次是什么意思呢?那么这次是直接运行完吗,既然这次都运行完了,又怎么会有下一次呢。。。这段不大理解,有点懵了。。。
作者回复: go 1.14版本之前不支持直接抢占,而是在函数或方法中“添加调度代码”,所以虽然设置了抢占标志,但是只有下一次运行“添加的调度代码”时,该G才会让出M。
- simple_孙2022-03-18每个P是不是跟Java中的线程一样都有一个就绪队列和等待队列呢
作者回复: 每个P只有一个队列。
- 不负青春不负己🤘2022-02-02mark
作者回复: 👍