【Go】Golang runtime调度③

文章目录

    • 进程、线程、协程
      • 进程和线程
      • 线程与 Goroutine
  • Goroutine概念
    • goroutine 和 thread 的区别?
      • 内存占用
      • 创建/销毁,
      • 调度切换
      • 复杂性
    • M:N 线程、协程模型
    • Goroutine Lifecycle
  • runtime 调度器
    • 触发调度时间点
      • runtime.gopark (触发调度)
      • 系统调用
      • 协作式调度
    • 抢占式调度器
      • 协作的抢占式调度的工作原理
      • 基于信号的抢占式调度(异步抢占)
    • 任务窃取调度器
    • runtime.schedule 调度流程
  • GM调度器(go1.2 早期调度)
    • GM 调度模型的问题
  • GMP调度模型
    • GMP各自定义
      • G结构体及状态
        • G状态流转
      • M结构与状态
        • g0
      • P结构与状态
    • Work-stealing
      • 本地队列、全局队列
      • G什么时候会被放到全局队列?
    • 触发调度(阻塞场景)
      • 1. 由于原子、互斥量或通道操作调用 主动让出
      • 2.由于网络请求和 IO 操作导致 Goroutine 阻塞
    • 抢占调度
      • time.sleep 如何知道 需要抢占P (sysmon监控线程)
      • 系统调用产生的IO阻塞(文件IO等)
    • syscall 系统调用
      • P 和 M Syscall恢复后执行流程
    • Spining thread 自旋线程和非自旋线程
  • 网络轮询器 NetPoller
      • netpoller
    • 那什么时候 G 被调度回来呢?(从netpoll取G)
    • 事件循环
    • 轮询等待
      • NetPoller(网络轮询器)
    • I/O 多路复用
      • netpoll返回的Goroutine 列表改怎么办?
      • 文件IO会阻塞线程?
  • 系统监控 sysmon
    • sysmon 函数到底做了什么?
      • sysmon轮询网络(netpoll)
      • 信号抢占
  • Go 程序启动
    • OS thread 创建
    • 特殊的 g0
    • Schedule
  • Go调度总结
  • 面试问题
      • 自旋线程M的最大限制
      • 3.调度器的设计策略
      • 4.Go调度本质
        • 1.当一个goroutine被网络IO阻塞住后,它对应的线程会被阻塞吗?
        • 2,如果有的M较忙,有的M较闲呢?
        • 3,如果一个G运行时间过长>10ms(一直占用资源怎么办),导致队列中后续G都无法运行呢?
        • 4,一个G由于调度被中断,此后如何恢复?
        • 5,一个G自旋,被调度器抢占的时机?
        • 6.runtime goroutine执行调度
          • runqput、runqget、runnext
          • 本地runq被填满
          • goroutine执行顺序:
        • 5.如何解决Go服务容器化的P数量的问题
  • 四、go为什么这么快?(再探GMP模型)
      • 进程、线程、协程
    • goroutine 和 thread 的区别
      • go协程调用跟切换比线程效率高
      • 内存消耗
      • 区别

https://www.cnblogs.com/qcrao-2018/p/11442998.html

为什么要使用 Go 语言?Go 语言的优势在哪里?

goroutine协程让出、抢占、监控、调度

进程、线程、协程

进程和线程

多个线程可以属于同一个进程并共享内存空间。因为多线程不需要创建新的虚拟内存空间,所以它们也不需要内存管理单元处理上下文的切换,线程之间的通信也正是基于共享的内存进行的,与重量级的进程相比,线程显得比较轻量。

虽然线程比较轻量,但是在调度时也有比较大的额外开销。每个线程会都占用 1M 以上的内存空间,在切换线程时不止会消耗较多的内存,恢复寄存器中的内容还需要向操作系统申请或者销毁资源,每一次线程上下文的切换都需要消耗 ~1us 左右的时间1,但是 Go 调度器对 Goroutine 的上下文切换约为 ~0.2us,减少了 80% 的额外开销2。

线程与 Goroutine

Go 语言的调度器通过使用与 CPU 数量相等的线程减少线程频繁切换的内存开销,同时在每一个线程上执行额外开销更低的 Goroutine 来降低操作系统和硬件的负载。

Goroutine概念

Goroutine 是一个与其他 goroutines 并行运行在同一地址空间的 Go 函数或方法。一个运行的程序由一个或更多个
goroutine 组成。它与线程、协程、进程等不同。它是一个 goroutine” —— Rob Pike

Goroutines 在同一个用户地址空间里并行独立执行 functions,channels 则用于 goroutines 间的通信和同步访问控制。
【Go】Golang runtime调度③_第1张图片

goroutine 和 thread 的区别?

【Go】Golang runtime调度③_第2张图片

内存占用

创建一个 goroutine 的栈内存消耗为 2 KB(Linux AMD64 Go v1.4后),运行过程中,如果栈空间不够用,会自动进行扩容。

创建一个 thread 为了尽量避免极端情况下操作系统线程栈的溢出,默认会为其分配一个较大的栈内存( 1 - 8 MB 栈内存,线程标准 POSIX Thread),而且还需要一个被称为 “guard page” 的区域用于和其他 thread 的栈空间进行隔离。而栈内存空间一旦创建和初始化完成之后其大小就不能再有变化,这决定了在某些特殊场景下系统线程栈还是有溢出的风险。

创建/销毁,

线程创建和销毀都会有巨大的消耗,是内核级的交互(trap)。

POSIX 线程(定义了创建和操纵线程的一套 API)通常是在已有的进程模型中增加的逻辑扩展,所以线程控制和进程控制很相似。而进入内核调度所消耗的性能代价比较高,开销较大。goroutine 是用户态线程,是由 go runtime 管理,创建和销毁的消耗非常小。

调度切换

抛开陷入内核,线程切换会消耗 1000-1500 纳秒(上下文保存成本高,较多寄存器,公平性,复杂时间计算统计),一个纳秒平均可以执行 12-18 条指令。
所以由于线程切换,执行指令的条数会减少 12000-18000。goroutine 的切换约为 200 ns(用户态、3个寄存器),相当于 2400-3600 条指令。因此,goroutines 切换成本比 threads 要小得多。
另外,goroutines是协作式调度

复杂性

线程的创建和退出复杂,多个 thread 间通讯复杂(share memory)。
不能大量创建线程(参考早期的 httpd),成本高,使用网络多路复用,存在大量callback(参考twemproxy、nginx 的代码)。对于应用服务线程门槛高,例如需要做第三方库隔离,需要考虑引入线程池等。

M:N 线程、协程模型

Go 创建 M 个线程(CPU 执行调度的单元,内核的 task_struct),之后创建的 N 个 goroutine 都会依附在这 M 个线程上执行,即 M:N 模型。它们能够同时运行,与线程类似,但相比之下非常轻量。因此,程序运行时,Goroutines 的个数应该是远大于线程的个数的(phread 是内核线程?)。

同一个时刻,一个线程只能跑一个 goroutine。当 goroutine 发生阻塞 (chan 阻塞、mutex、syscall 等等) 时,Go 会把当前的 goroutine 调度走,让其他 goroutine 来继续执行,而不是让线程阻塞休眠,尽可能多的分发任务出去,让 CPU 忙。

Go 创建 M 个线程(CPU 执行调度的单元,内核的 task_struct),之后创建的 N 个 goroutine 都会依附在这 M 个线程上执行,即 M:N 模型。它们能够同时运行,与线程类似,但相比之下非常轻量。因此,程序运行时,Goroutines 的个数应该是远大于线程的个数的(phread 是内核线程?)。

同一个时刻,一个线程只能跑一个 goroutine。当 goroutine 发生阻塞 (chan 阻塞、mutex、syscall 等等) 时,Go 会把当前的 goroutine 调度走,让其他 goroutine 来继续执行,而不是让线程阻塞休眠,尽可能多的分发任务出去,让 CPU 忙。

Goroutine Lifecycle

start

启动 m0 主线程,初始化 g0 负责 schedule,初始化 P, sysmon 线程, GC 协程;

然后 P 负责创建 os thread 关联 M;

G 切换时,暂存当前的 PC 及 GO 堆栈即可;

回收:G 用完会放到空闲列表,由 P 来负责回收

runtime 调度器

触发调度时间点

【Go】Golang runtime调度③_第3张图片

runtime.gopark (触发调度)

runtime.gopark 是触发调度最常见的方法,该函数会将当前 Goroutine 暂停,被暂停的任务不会放回运行队列,我们来分析该函数的实现原理:

函数 诱发的因素 主要场景为:

  • 通道(Channel)。
  • 垃圾回收(GC)。
  • 休眠(Sleep)。
  • 锁等待(Lock)。
  • 抢占(Preempted)。
  • IO 阻塞(IO Wait)
  • 其他,例如:panic、finalizer、select 等。

我们可以根据这些特性,去拆解可能会造成阻塞的原因。其实也就没必要记了,他们会导致阻塞肯定是由于存在影响控制流的因素,才会导致 gopark 的调用。

runtime.park_m 会将当前 Goroutine 的状态从 _Grunning 切换至 _Gwaiting,调用 runtime.dropg 移除线程和 Goroutine 之间的关联,在这之后就可以调用 runtime.schedule 触发新一轮的调度了。

当 Goroutine 等待的特定条件满足后,运行时会调用 runtime.goready 将因为调用 run

你可能感兴趣的:(Go,后端开发,#,Go基础知识,golang,java)