• 【协作式原创】查漏补缺之Goroutine的调度


    注: 只讲了调度器,不涉及任何GC相关知识

    预备知识

    回顾一下Go调度器的发展历程

    验证真抢占式调度是哪个版本引入的

    Go Version Manager用于切换不同版本的Go
    这里说到1.14之前都是请求抢占,1.14才是真抢占

    Scheduler的宏观组成

    参考附录11

    • Go调度器的主要职责就是将G公平合理的安排到多个M上去执行

    共同梳理的几个问题

    1. N:1 1:1 M:N 模型及其优缺点

    • 参考附录9
    • 参考附录14
      用户线程与KSE是1对1关系(1:1)。大部分编程语言的线程库(如linux的pthread,Java的java.lang.Thread,C++11的std::thread等等)都是对操作系统的线程(内核级线程)的一层封装,创建出来的每个线程与一个不同的KSE静态关联,因此其调度完全由OS调度器来做。

    2. GPM模型

    参考附录1

    G: 提供要执行的用户代码。
    M: 对应一个内核线程(LWP(tgid!=pid)),提供CPU资源。每个M必需绑定一个P才能运行G。M的数量比P多,一般不会多太多,最多10000个M(源码可看到) // 参考附录2图片
    P: 提供执行G的其他资源,比如(本地可运行G队列,内存分配用到的缓存mcache)。

    • G的创建
      当通过go关键字创建一个新的goroutine的时候,它会优先被放入P的本地队列。

    • G的存放位置
      Go调度器工作时会维护两种用来保存G的任务队列:一种是一个Global任务队列,一种是每个P维护的Local任务队列。

    • G的选取和work-stealing
      当M执行完了当前P的Local队列里的所有G后,P也不会就这么在那躺尸啥都不干,它会先尝试从Global队列寻找G来执行,如果Global队列为空,它会随机挑选另外一个P,从它的队列里中拿走一半的G到自己的队列中执行。
      为了公平,调度器每调度 61 次的时候,都会尝试从全局队列里取出待运行的 goroutine 来运行。// 参考附录4(搜索61)

    3. 怎么调度的?

    TODO: 考虑回答 G出生,工作窃取,G阻塞 这3种情况的调度器处理?

    4. Q: 大量goroutine时的调度开销

    其他问题

    抢占相关的问题 // 参考附录7-场景12

    Q: 哪个版本实现了抢占调度?
    Go调度在go1.12实现了抢占,应该更精确的称为请求式抢占?

    Q: Go的抢占和操作系统的抢占有何不同?
    请求抢占和强制抢占

    Q: 什么情况会发送抢占请求。
    A: 单个操作执行过久和运行时间过长。具体的:
    抢占请求需要满足2个条件中的1个:1)G进行系统调用超过20us,2)G运行超过10ms。

    Q: 谁来发送抢占请求
    A: 启动的时候会启动一个单独的线程sysmon,它负责所有的监控工作,其中1项就是抢占,发现满足抢占条件的G时,就发出抢占请求。

    Q: 说一下抢占调度
    A: Go调度在go1.12实现了抢占,应该更精确的称为请求式抢占,那是因为go调度器的抢占和OS的线程抢占比起来很柔和,不暴力,不会说线程时间片到了,或者更高优先级的任务到了,执行抢占调度。go的抢占调度柔和到只给goroutine发送1个抢占请求,至于goroutine何时停下来,那就管不到了。抢占请求需要满足2个条件中的1个:1)G进行系统调用超过20us,2)G运行超过10ms。调度器在启动的时候会启动一个单独的线程sysmon,它负责所有的监控工作,其中1项就是抢占,发现满足抢占条件的G时,就发出抢占请求。

    常见问题列表 TODO

    参考附录1

    12个问题 // 附录8

    1. 为什么需要Go调度器?

    我们需要内核调度器调度进程和内核线程,也就需要Go调度器调度协程(用户线程)。

    • 内核线程和用户线程(协程)
      协程的优点: 在用户态线程即完成切换,不会陷入到内核态,这种切换非常的轻量快速。协程切换3个寄存器,内核线程需要切换几个寄存器和其他东西。//附录10

    2. Go调度器与系统调度器有什么区别和关系/联系?

    3. G、P、M是什么,三者的关系是什么?

    4. P有默认几个?

    所有的P都在程序启动时创建(也就是默认),并保存在数组中,最多有GOMAXPROCS个。

    5. M同时能绑定几个P?

    应该是1个

    6. M怎么获得G?

    参考附录11("Scheduler的宏观组成"的解释部分),附录12(源码的3个函数)

    7. M没有G怎么办?

    参考附录7-场景9
    A: 自旋
    为什么自旋? 我们希望当有新goroutine创建时,立刻能有m运行它,如果销毁再新建就增加了时延,降低了效率。
    没有G的M都自旋吗? 当然也考虑了过多的自旋线程是浪费CPU,所以系统中最多有GOMAXPROCS个自旋的线程,多余的没事做线程会让他们休眠。

    8. 为什么需要全局G队列?

    和本地

    9. Go调度器中的负载均衡的2种方式是什么?

    10. work stealing是什么?什么原理?

    done

    11. 系统调用对G、P、M有什么影响?

    done

    12.Go调度器抢占是什么样的?一定能抢占成功吗?

    done

    Go抢占调度问题三连

    1. 我有个问题啊?go的抢占调度是让goroutine自己退出嘛
    2. 调度器会向这个goroutine发指令?
    3. 这个怎么做的?// 参考附录7(场景12)
    • 抢占的时机: 在函数调用前扩展栈时顺便查看自己的抢占flag,决定是否继续执行,还是让出自己。// PS. 也就是说,如果G没有函数调用,那么就会一直执行。

    golang高并发模型

    • Q: G的调度时机
      参考附录13

    Q: 死循环的问题是那个版本中处理的,如何处理的? // TODO

    其他可能有用的

    设计问题: 为什么GM要引入P

    参考附录6论文.
    A1: 官方解释,感觉很抽象,不好理解。
    One of Vyukov’s plans is to create a layer of abstraction. He proposes to include another struct, P, to simulate processors.
    An M would still represent an OS thread, and a G would still portray a goroutine.
    There are exactly GOMAXPROCS P’s, and a P would be another required resource for an M in order for that M to execute Go code.

    A2: 比起为了增加中间层而引入P,我觉得下面的理由更合理的解释了为什么引入P。
    为了减小全局锁的影响,所以需要把全局队列拆分,拆分之后需要存放到一个地方,这个时候P就出现了。

    出处 https://www.bilibili.com/video/av64926593 的 9分30秒左右

    A3: 我的理解
    我觉得第二种理解其实更符合我们的认知:
    如果原来是一把大锁,锁定了全局协程队列这样一个共享资源,导致了很多激烈的锁竞争。
    如果我们要优化,很自然的就能想到,把锁的粒度细化来提高减少锁竞争,也就是把全局队列拆除局部队列。具体实施的时候会有一下几个问题。

    1. 这些局部队列放在哪儿呢?我们引入了一个P用来放局部队列。
    2. 应该拆分为几个局部队列呢? n核就拆分为n个局部队列,因为最多能有n个任务同时执行,这样既能做到最大的并行度,有使得局部队列之间没有竞争。
      顺着这个思路,我们发现工作窃取也是很合理的,比如P1执行完本地队列,没必要闲着,可以去其他P的本地队列里偷任务来执行,这样可以更好的在可用的处理器之间分配任务。

    A4: 其实就是借鉴linux2.4-linux2.6内核调度器优化的过程
    参考: 关于中的Scalable>

    参考资料

    1. GPM调度
    2. 深度解密Go语言之 scheduler
    3. 新官上任 —— Go sheduler 开始调度循环(五)
      深度解密调度器源码系列
    4. GO SCHEDULER: MS, PS & GS
      =>
    5. Analysis of the Go runtime scheduler
    6. Go调度器系列(3)图解调度原理
    7. Go调度器系列(4)源码阅读与探索
    8. Go调度器系列(1)起源
    9. 简单理解 Goroutine 是如何工作的
    10. Go调度器系列(2)宏观看调度器
    11. 锲而不舍 —— M 是怎样找工作的?(八)
    12. 看这2小节: goroutine调度时机和同步异步的系统调用
    13. 线程的实现模型主要有3个,分别是:用户级线程模型、内核级线程模型和两级线程模型

    为什么进程需要阻塞态
    三足鼎立 —— GPM 到底是什么?(一)
    go语言调度器源代码情景分析

  • 相关阅读:
    xgqfrms™, xgqfrms® : xgqfrms's offical website of GitHub!
    xgqfrms™, xgqfrms® : xgqfrms's offical website of GitHub!
    xgqfrms™, xgqfrms® : xgqfrms's offical website of GitHub!
    xgqfrms™, xgqfrms® : xgqfrms's offical website of GitHub!
    xgqfrms™, xgqfrms® : xgqfrms's offical website of GitHub!
    Activiti任务参数和流程参数的对比
    Intel x710万兆 SR-IOV 网卡驱动升级
    git clone分支到本地
    资源丨MySQL故障排查思路方法PPT&视频&24问答
    win10系统端口转发
  • 原文地址:https://www.cnblogs.com/yudidi/p/12380871.html
Copyright © 2020-2023  润新知