Tony Bai · Go 语言第一课
Tony Bai
资深架构师,tonybai.com 博主
21492 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 59 讲
开篇词 (1讲)
结束语 (1讲)
Tony Bai · Go 语言第一课
15
15
1.0x
00:00/00:00
登录|注册

32|并发:聊聊Goroutine调度器的原理

main goroutine调度情况
多核处理器主机上运行
抢占式调度逻辑
sysmon监控线程
M: 执行计算资源
P: 逻辑processor,队列和缓存
G: Goroutine信息
基于信号的异步抢占(Go 1.14)
协作式抢占(Go 1.2)
伸缩性提升
P(逻辑Processor)引入
Go 1.1实现
GOMAXPROCS变量控制
G(Goroutine)和M(machine)结构
Go 1.0实现
调度切换代价低
Goroutine栈大小默认2KB
Go调度器调度Goroutine
操作系统调度进程和线程
将Goroutine调度到CPU执行
注意事项
Goroutine基本使用
示例代码分析
系统调用阻塞
channel阻塞或网络I/O
G的抢占调度
G、P、M定义
抢占式调度
G-P-M模型
G-M模型
资源占用
与操作系统调度器对比
概念
Go并发方案
思考题
特殊情况下的G调度
G-P-M模型深入
调度器模型与演化
Goroutine调度器
并发基本概念
Goroutine调度器原理

该思维导图由 AI 生成,仅供参考

你好,我是 Tony Bai。
上一讲我们学习了并发的基本概念和 Go 的并发方案,也就是 Goroutine 的一些基本使用和注意事项。对于大多数 Gopher 来说,这些内容作为 Go 并发入门已经是足够了。
但毕竟 Go 没有采用基于线程的并发模型,可能很多 Gopher 都好奇 Go 运行时究竟是如何将一个个 Goroutine 调度到 CPU 上执行的。当然,Goroutine 的调度本来是 Go 语言核心开发团队才应该关注的事情,大多数 Gopher 们无需关心。但就我个人的学习和实践经验而言,我觉得了解 Goroutine 的调度模型和原理,能够帮助我们编写出更高质量的 Go 代码。
因此,在这一讲中,我想和你一起简单探究一下 Goroutine 调度器的原理和演化历史。

Goroutine 调度器

提到“调度”,我们首先想到的就是操作系统对进程、线程的调度。操作系统调度器会将系统中的多个线程按照一定算法调度到物理 CPU 上去运行。
前面我们也提到,传统的编程语言,比如 C、C++ 等的并发实现,多是基于线程模型的,也就是应用程序负责创建线程(一般通过 libpthread 等库函数调用实现),操作系统负责调度线程。当然,我们也说过,这种传统支持并发的方式有很多不足。为了解决这些问题,Go 语言中的并发实现,使用了 Goroutine,代替了操作系统的线程,也不再依靠操作系统调度。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

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_cca544
    go1.13的话加上runtime.GOMAXPROCS(1) main goroutine在创建 deadloop goroutine 之后就无法继续得到调度 但如果是go1.14之后的话即使加上runtime.GOMAXPROCS(1) main goroutine在创建 deadloop goroutine 之后还是可以得到调度,应该是因为增加了对非协作的抢占式调度的支持

    作者回复: ✅

    2022-01-11
    48
  • 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-13
    31
  • 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-15
    18
  • 麦芒极客
    老师您好,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-22
    17
  • bearlu
    老师,想学习goroutine调度器,演进的关键版本,依次是go的什么发行版?还有什么相关资料书籍?谢谢老师

    作者回复: 欧长坤老师维护的这个go history中有关调度器的演化可以参考:https://golang.design/history/#scheduler

    2022-01-10
    2
    6
  • 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-10
    2
    3
  • 微微超级丹💫
    请问常规文件是不支持监听的(pollable)是什么意思呀?

    作者回复: 就是说对常规文件的读写操作(因为是系统调用)依然会阻塞M。

    2022-09-20归属地:北京
    2
  • 路边的猪
    第一种,因为网络io导致阻塞的处理方式这里。我想问,网络io势必会引起系统调用,比如最基础的建立tcp连接这些,那这块儿是咋区分系统调用和网络io的呢?

    作者回复: 由于go运行时netpoller的存在,我们很难精确区分。这种“网络io”会被runtime转换为goroutine的调度等,不一定真的实施网络io。

    2022-06-05
    2
  • 多选参数
    有几个问题想问老师: 1. 文章中提到的 Go 运行时这块更多是指哪些内容?Go 调度器应该也是 Go 运行时中的部分内容吧?就是 Go 开源的源代码吗? 2. 有关 Go 运行时比较好的资料推荐吗?比如 Go 运行时的架构、Go 运行时是怎么和用户编写的代码合在一起的,因为我理解的 Go 运行时不像 JVM 那样,Go 是编译性的,也就是说会将用户编写的代码和 Go 运行时代码一起编译成二进制,然后运行是从 Go 运行时开始的,然后在从用户逻辑的代码开始。 老师有空的时候麻烦解答下,谢谢老师~

    作者回复: 《Go语言第一课FAQ》中有这方面的解答,你可以先看看。 https://tonybai.com/go-course-faq

    2022-01-29
    2
收起评论
显示
设置
留言
25
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部