首页 > 其他分享 >Go语言精进之路读书笔记第32条——了解goroutine的调度原理

Go语言精进之路读书笔记第32条——了解goroutine的调度原理

时间:2024-02-22 09:44:59浏览次数:39  
标签:协程 读书笔记 队列 32 goroutine 调度 线程 执行

Go的运行时负责对goroutine进行管理,所谓的管理就是“调度”。调度就是决定何时哪个goroutine将获得资源开始执行,哪个goroutine应该停止执行让出资源,哪个goroutine应该被唤醒恢复执行等。

32.1 goroutine调度器

  • 将goroutine按照一定算法放到CPU上执行的程序就称为goroutine调度器(goroutine scheduler)
  • 一个Go程序对于操作系统来说只是一个用户层程序,操作系统眼中只有线程,goroutine的调度全要靠Go自己完成

32.2 goroutine调度模型与演进过程

1.GM模型

  • Go1.0版本
  • G(goroutine):每个goroutine对应于运行时的抽象结构,M(machine):操作系统线程的抽象接口
  • 单一全局互斥锁(Sched.Lock)和集中状态存储的存在导致所有goroutine相关操作(如创建、重新调度等)都要上锁
  • goroutine传递问题:经常在M之间传递“可运行”的goroutine会导致调度延迟增大,带来额外的性能损耗
  • 每个M都做内存缓存,导致内存占用过高,数据局部性变差
  • 因系统调用(syscall)而形成的频繁的工作线程阻塞和解除阻塞会带来额外的性能损耗

2.GMP模型

  • Go1.1版本
  • P:逻辑处理器,每个G想要真正运行起来,首先需要被分配一个P,即进入P的本地运行队列(local queue)中
  • 只有将P和M绑定才能让P的本地队列中的G真正运行起来。P和M的关系就好比Linux操作系统调度层面用户线程(user thread)与内核线程(kernel thread)的对应关系:多对多(N:M)

3.抢占式调度

  • 不支持抢占式调度,导致一旦某个G中出现死循环的代码逻辑,那么G将永久占用分配给它的P和M,而位于同一个P中的其他G将得不到调度,出现“饿死”的情况
  • 抢占式调度的原理:在每个函数或方法的入口加上一段额外的代码,让运行时有机会检查是否需要执行抢占调度
  • 局部解决了“饿死”问题,对于没有函数调用而是纯算法循环计算的G,goroutine调度器依然无法抢占(Go1.14版本中加入了基于系统信号的goroutine抢占式调度机制,很大程度上解决了goroutine“饿死”的问题)

4.NUMA调度模型

5.其他优化

  • Go1.9版本增加了一个针对文件I/O的Poller,它可以像netpoller那样,在G操作那些支持监听的(pollable)文件描述符时,仅阻塞G,而不会阻塞M

32.3 对goroutine调度器原理的进一步理解

1.GMP

1.G
(1)g 即goroutine,是 golang 中对协程的抽象
(2)g 有自己的运行栈、状态、以及执行的任务函数(用户通过 go func 指定)
(3)g 需要绑定到 p 才能执行,在 g 的视角中,p 就是它的 cpu

2.M
(1)m 即 machine,是 golang 中对线程的抽象
(2)m 不直接执行 g,而是先和 p 绑定,由其实现代理
(3)借由 p 的存在,m 无需和 g 绑死,也无需记录 g 的状态信息,因此 g 在全生命周期中可以实现跨 m 执行

3.P
(1)p 即 processor,是 golang 中的调度器
(2)p 是 gmp 的中枢,借由 p 承上启下,实现 g 和 m 之间的动态有机结合
(3)对 g 而言,p 是其 cpu,g 只有被 p 调度,才得以执行
(4)对 m 而言,p 是其执行代理,为其提供必要信息的同时(可执行的 g、内存分配情况等),并隐藏了繁杂的调度细节
(5)p 的数量决定了 g 最大并行数量,可由用户通过 GOMAXPROCS 进行设定(超过 CPU 核数时无意义)

4.GMP
(1)M 是线程的抽象;G 是 goroutine;P 是承上启下的调度器
(2)M调度G前,需要和P绑定
(3)全局有多个M和多个P,但同时并行的G的最大数量等于P的数量
(4)G的存放队列有三类:P的本地队列;全局队列;和wait队列(图中未展示,为io阻塞就绪态goroutine队列)
(5)M调度G时,优先取P本地队列,其次取全局队列,最后取wait队列;这样的好处是,取本地队列时,可以接近于无锁化,减少全局锁竞争
(6)为防止不同P的闲忙差异过大,设立work-stealing机制,本地队列为空的P可以尝试从其他P本地队列偷取一半的G补充到自身队列

2.G被抢占调度

如果某个G没有进行系统调用(syscall)、没有进行I/O操作、没有阻塞在一个channel操作上,那么M是如何让G停下来并调度下一个可运行的G的呢?答案是:G是被抢占调度的。

Go程序启动时,运行时会启动一个名为sysmon的M(一般称为监控线程),该M的特殊之处在于它无须绑定P即可运行(以g0这个G的形式)

3.channel阻塞或网络I/O情况下的调度

如果G被阻塞在某个channel操作或网络I/O操作上,那么G会被放置到某个等待队列中,而M会尝试运行P的下一个可运行的G

  • 如果此时P没有可运行的G供M运行,那么M将解绑P,并进入挂起状态
  • 当I/O操作完成或channel操作完成,在等待队列中的G会被唤醒,标记为runnable

4.系统调用阻塞情况下的调度

G会阻塞,执行该G的M也会解绑P(实质是被sysmon抢走了),与G一起进入阻塞状态

32.4 调度器状态的查看方法

使用Go运行时环境变量GODEBUG

32.5 goroutine调度实例简要分析

  1. 为何在存在死循环的情况下,多个goroutine依旧会被调度并轮流执行?

根本原因在于机器是多核多线程的

  1. 如何让deadloop goroutine以外的goroutine无法得到调度?

调整GOMAXPROCS;单核单线程的机器

  1. 反转:如何在GOMAXPROCS=1的情况下让main goroutine得到调度
func add(a, b int) int {
    return a + b
}

func dummy() {
    add(3, 5)
}

func deadloop() {
    for {
        dummy()
    }
}

func main() {
    runtime.GOMAXPROCS(1)
    go deadloop()
    for {
        time.Sleep(time.Second * 1)
        fmt.Println("I got scheduled!")
    }
}

补充1:基础概念梳理

1.1 线程

通常语义中的线程,指的是内核级线程,核心点如下:
(1)是操作系统最小调度单元
(2)创建、销毁、调度交由内核完成,cpu 需完成用户态与内核态间的切换
(3)可充分利用多核,实现并行

1.2 协程

协程,又称为用户级线程,核心点如下:
(1)与线程存在映射关系,为 M:1
(2)创建、销毁、调度在用户态完成,对内核透明,所以更轻
(3)从属同一个内核级线程,无法并行;一个协程阻塞会导致从属同一线程的所有协程无法执行

1.3 Goroutine

Goroutine,经 Golang 优化后的特殊“协程”,核心点如下:
(1)与线程存在映射关系,为 M:N
(2)创建、销毁、调度在用户态完成,对内核透明,足够轻便
(3)可利用多个线程,实现并行
(4)通过调度器的斡旋,实现和线程间的动态绑定和灵活调度
(5)栈空间大小可动态扩缩,因地制宜

补充2:数据结构

1.G

type g struct {
    // ...
    m         *m      
    // ...
    sched     gobuf
    // ...
}

type gobuf struct {
    sp   uintptr
    pc   uintptr
    ret  uintptr
    bp   uintptr // for framepointer-enabled architectures
}

(1)m:在 p 的代理,负责执行当前 g 的 m
(2)sched.sp:保存 CPU 的 rsp 寄存器的值,指向函数调用栈栈顶
(3)sched.pc:保存 CPU 的 rip 寄存器的值,指向程序下一条执行指令的地址
(4)sched.ret:保存系统调用的返回值
(5)sched.bp:保存 CPU 的 rbp 寄存器的值,存储函数栈帧的起始位置

其中 g 的生命周期由以下几种状态组成:

const(
  _Gidle = itoa // 0
  _Grunnable // 1
  _Grunning // 2
  _Gsyscall // 3
  _Gwaiting // 4
  _Gdead // 6
  _Gcopystack // 8
  _Gpreempted // 9
)

(1)_Gidle 值为 0,为协程开始创建时的状态,此时尚未初始化完成
(2)_Grunnable 值 为 1,协程在待执行队列中,等待被执行
(3)_Grunning 值为 2,协程正在执行,同一时刻一个 p 中只有一个 g 处于此状态
(4)_Gsyscall 值为 3,协程正在执行系统调用
(5)_Gwaiting 值为 4,协程处于挂起态,需要等待被唤醒. gc、channel 通信或者锁操作时经常会进入这种状态
(6)_Gdead 值为 6,协程刚初始化完成或者已经被销毁,会处于此状态
(7)_Gcopystack 值为 8,协程正在栈扩容流程中
(8)_Greempted 值为 9,协程被抢占后的状态

2.M

type m struct {
    g0      *g     // goroutine with scheduling stack
    // ...
    tls           [tlsSlots]uintptr // thread-local storage (for x86 extern register)
    // ...
}

(1)g0:一类特殊的调度协程,不用于执行用户函数,负责执行 g 之间的切换调度. 与 m 的关系为 1:1
(2)tls:thread-local storage,线程本地存储,存储内容只对当前线程可见. 线程本地存储的是 m.tls 的地址,m.tls[0] 存储的是当前运行的 g,因此线程可以通过 g 找到当前的 m、p、g0 等信息

3.P

type p struct {
    // ...
    runqhead uint32
    runqtail uint32
    runq     [256]guintptr
    
    runnext guintptr
    // ...
}

(1)runq:本地 goroutine 队列,最大长度为 256
(2)runqhead:队列头部
(3)runqtail:队列尾部
(4)runnext:下一个可执行的 goroutine

4.schedt

type schedt struct {
    // ...
    lock mutex
    // ...
    runq     gQueue
    runqsize int32
    // ...
}

sched 是全局 goroutine 队列的封装
(1)lock:一把操作全局队列时使用的锁
(2)runq:全局 goroutine 队列
(3)runqsize:全局 goroutine 队列的容量

参考

go语言学习笔记(三):调度器基础-走近那座山
Golang GMP 原理
深入分析Go1.18 GMP调度器底层原理

标签:协程,读书笔记,队列,32,goroutine,调度,线程,执行
From: https://www.cnblogs.com/brynchen/p/18026642

相关文章

  • P8329 [ZJOI2022] 树
    直接求是困难的,所以考虑容斥将所求容斥为两部分:每个结点至少在一棵树上为叶子的方案数-至少有一个结点在两棵树上都为叶子的方案数。考虑DP,设\(f_i(x,y)\)表示\([1,i]\)中是第一棵树的非叶子的结点数为\(x\),\([i+1,n]\)中是第二棵树的非叶子的结点数为\(y\)时的......
  • Go语言精进之路读书笔记第31条——优先考虑并发设计
    31.1并发与并行1.并行方案在处理器核数充足的情况下启动多个单线程应用的实例2.并发方案重新做应用结构设计,即将应用分解成多个在基本执行单元(例如操作系统线程)中执行的、可能有一定关联关系的代码片段goroutine:由Go运行时负责调度的用户层轻量级线程,相比传统操作系统线程而......
  • 《程序是怎样跑起来的》第二章读书笔记
    32位是4个字节,反转部分图形模式时,使用的是XOR运算.CPU和内存是IC的一种,IC的所有引脚只有直流电压0V和5V两个状态。IC的这个特性决定了计算机的信息数据只能由二进制数来处理。计算机处理信息的最小单位——位(bit)。八位二进制数被称为一个字节,字节是最基本的信息计量单位。位是最......
  • 《程序是怎样跑起来的》第一章读书笔记
    一个CPU中有许多寄存器,控制器,运算器,时钟等,其都富含各种特定功能,CPU是寄存器的集合体,程序是把寄存器作为对象来描述的。汇编就是汇编语言编写的程序转化为机器语言的过程,使用高级语言编写的程序会在编译后转化为机器语言,然后再通过CPU内部的寄存器来处理。不同类型的CPU,其内部寄存......
  • Go 100 mistakes - #32: Ignoring the impact of using pointer elements in range lo
    Thissectionlooksataspecificmistakewhenusingarangeloopwithpointerelements.Ifwe’renotcautiousenough,itcanleadustoanissuewherewereferencethe wrongelements.Let’sexaminethisproblemandhowtofixit.Beforewebegin,let’scla......
  • 微控制器STM32L475RCT7[IC MCU 32BIT 256KB]、AZ5A25-01F.R7G瞬态抑制二极管(TVS),AONS
    1、微控制器STM32L475RCT7[ICMCU32BIT256KBFLASH64LQFP]STM32L475RC器件是基于高性能ARM®Cortex®-M432位RISC内核的超低功耗微控制器,工作频率高达80MHz。Cortex-M4内核具有浮点单元(FPU)单精度,支持所有ARM单精度数据处理指令和数据类型。它还实现了完整的DSP指令集和存储......
  • Go语言精进之路读书笔记第30条——使用接口提高代码的可测试性
    Go语言有一个惯例是让单元测试代码时刻伴随着你编写的Go代码。单元测试是自包含和自运行的,运行时一般不会依赖外部资源(如外部数据库、外部邮件服务器等),并具备跨环境的可重复性(既可在开发人员的本地运行,也可以在持续集成的环境中运行)。30.1实现一个附加免责声明的电子邮件发送函......
  • 【三分钟开服仅32/月】幻兽帕鲁服务器最新一键部署以及修改游戏参数保姆教程
    前言《幻兽帕鲁》是Pocketpair开发的一款开放世界生存制作游戏,游戏于2023年11月2日至11月5日进行了封闭网络测试,于2024年1月18日发行抢先体验版本。游戏中,玩家可以在广阔的世界中收集神奇的生物“帕鲁”,派他们进行战斗、建造、做农活,工业生产等游戏支持单人游玩&创建本地房间(最......
  • STM32 ---SPI通讯
    I2C能够通过软件模拟,同样的,SPI通讯也可以通过软件模拟,具体需要掌握SPI的收发时序。但在本节,我们着重讲解STM32的硬件SPI外设。 我们知道SPI有以下几个特点1、时钟频率:Fpclk/2,4,8,16,32,64,128,256(Fpclk是时钟分频,APH2的Fpclk是72MHZ,APB1的Fpclk是36MHZ)2、支持多主机模型......
  • Go语言精进之路读书笔记第29条——使用接口作为程序水平组合的连接点
    如果说C++和Java是关于类型层次结构和类型分类的语言,那么Go则是关于组合的语言。——RobPike,Go语言之父“偏好组合,正交解耦”29.1一切皆组合在语言设计层面,Go提供了诸多正交的语法元素供后续组合使用,包括:Go语言无类型体系(typehierarchy),类型定义独立;方法和类型是正交......