GMP模型

目录

1.为什么引入协程?

2.早期的Go调度器

3.GMP模型简介

4.调度器的设计策略

4.1. work-stealing机制

4.2. hand-off机制(切换机制)

5. Go指令的调度流程

6. 调度器的生命周期

7. 场景分析

7.1.(场景1)G1创建G3

7.2.(场景2)G1执行完毕

7.3.(场景3-4-5)

7.4.(场景6)唤醒正在休眠的M

7.5.(场景7)被唤醒的M2从全局队列中获取批量G

7.6.(场景8)M2从M1中批量偷取G

7.7.(场景9)自旋线程的最大限制

7.8.(场景10)G发生系统调用/阻塞

7.9.(场景11)G发生系统阻塞,再变为非阻塞

8. Golang系统调用与阻塞处理 😄

8.1. 阻塞

8.1.1. Go阻塞的4种场景

8.2. 系统调用与调度机制

8.2.1.异步系统调用

8.2.2.同步系统调用


视频链接

1.为什么引入协程?

  1. 线程进程模型的弊端

    1. 为了解决多线程多进程频繁切换,导致的CPU浪费

    2. 多线程随着同步竞争(锁、竞争资源冲突),导致性能下降

    3. 占用内存:进程4GB、线程4MB

  2. 协程的优点

    1. 协程是用户态实现的,不需要经过内核态和用户态之间的切换,更加轻量

    2. 一个goroutine:几KB

    3. 灵活调度,切换成本低

2.早期的Go调度器

  1. 全局go协程队列,存放着M个协程g

  2. 有N个线程去全局go协程队列获取G执行,每次获取都需要加全局锁(锁竞争)

3.GMP模型简介

① M每次都先去获取P  ② P再去获取G

        一个线程M想执行协程G:M就要先去「空闲P队列」获取P,然后P和M绑定,之后P再依次去「本地协程队列、全局协程队列」获取G,将G交给线程M去执行

  1. G:协程

  2. M:thread线程(内核线程)

    1. 有一个M阻塞,会先从空闲M队列获取新的M,若没有,再去创建一个新的M

    2. 如果有M空闲,那么就会回收or放回空闲M队列

  3. P:processor处理器(每个P具有自己的协程本地队列,P管理了协程队列中的G)

    1. P的本地队列存放等待运行的G

    2. 优先将新创建的G存放在P的本地队列,本地队列满了,才会放到全局队列

  4. 除了P的本地队列,还有一个全局队列

          

数量

  1. M的数量:GO语言本身限定M的最大量是1w,一般设置为核心数(runtime/debug包中的SetMaxThreads函数来设置)

  2. P的数量问题

    1. 环境变量$GOMAXPROC,一般设置为 = 内核线程数/2

    2. 在程序中通过runtime.GOMAXPROCS()来设置

  3. G的数量问题

4.调度器的设计策略

复用线程、利用并行、抢占、全局G队列

4.1. work-stealing机制

概述

  • 场景:当本线程⽆可运⾏的G时,尝试「从其他线程绑定的P偷取G」
  • 获取的流程:
    • 本地队列获取任务
    • 全局队列获取任务
    • 其它M的本地队列窃取任务

 case1:从全局队列中steal协程G 

此时,M2内核线程绑定的P,没有协程G了,M1的P也没有协程G,但是全局队列中有空闲的G

        

        M2去全局队列中steal协程G3,存放在自己的P中

        

case2:从其他P中steal协程G

1. M1和P绑定,G1正在运行,M2线程是空闲的 

         

2. 此时M2想执行,那么将会从M1绑定的P的本地队列中steal协程

      

4.2. hand-off机制(切换机制)

概述

  • 场景:当本线程因为G进⾏系统调⽤阻塞时,线程M释放绑定的P,把P转移给其他空闲的线程执⾏
  • 流程:当G阻塞时,与该G绑定的M也会陷入阻塞,在阻塞之前,会先把M绑定的P转移给其他M',然后将CPU切换到M'去执行

1. 假设,此时M1绑定的P队列中正在执行的协程G1,执行了一个阻塞操作,比如read

        

2. hand-off执行过程

        2.1. 首先,创建一个线程or唤醒一个睡眠状态的线程,如M3

                 

        2.2. 将M1绑定的P,迁移到M3上

                

        2.3. 将G1与M1进行绑定,此时

                ① M1阻塞等待read事件的返回

                ② 内核线程切换到M3,通过P去获取本地队列中的G2,继续执行

        这样就完成了hand-off机制

                

5. Go指令的调度流程

1~2步骤:执行go func(),先创建一个G,优先放入P的本地队列,如果满了,放入全局队列(此时P已经存放到P的本地队列)

        

3步骤:此时M获取G:优先从M的本地队列P中获取G,如果为空,依次去全局队列其他M的本地队列P去偷取G。(当获取P成功后,将P与M进行绑定)

         

 4~6步骤:

        之后,M1调度协程G,执行G的func()函数(备注,每个G的运行时间不超过10ms,防止其他G被饿死)

        

此时,G执行,执行分为以下情况

        case1:G的执行时间片超时,即执行时间大于10ms,G会重新放到M1绑定的本地队列P中   

                 

        case2:func函数执行了systemcall\阻塞(如read、write),则会获取新的M(从休眠M空闲队列or创建一个M)

                

                若此时,M1的P队列还有很多G等待执行,因为M在执行G1时调用了systemcall\阻塞操作,所以,M1的P队列将交给新的M接管(hand-off机制)

                执行完后的效果:①M1和G1捆绑 ②M3接管了M1的P

                

        之后,与M1绑定的G1,因为处于阻塞状态,所以下一步会解除绑定关系,此时①M1销毁或者存放回休眠队列M中 ②G1放回全局队列中

                

6. 调度器的生命周期

M0

        1. 启动程序后编号为0的主线程

        2. 在全局变量runtime.m0中,不需要在heap上分配

        3. 负责执行初始化和启动第一个G

        4. 执行第一个G之后,M0就和其他的M一样了

G0

        1. 每次启动一个M,都会第一个创建的G

        2. G0仅用于负责调度G

        3. G0指向任何可执行函数

        4. 每一个M都会有一个自己的G0

        5. 在调度或系统调用时就会使用M会切换到G0,来调度

        6. M0的G0会放在全局空间

       

        

7. 场景分析

7.1.(场景1)G1创建G3

此时,存在M1、M2,每个M绑定了P,P上分别有一个G

此时,G1创建了G3:满足局部性,即G1创建的G3,应该存放在M1和G1所在的P上(如下图所示)

 

7.2.(场景2)G1执行完毕

当M1绑定的G1执行goexit(),G1执行完毕:M1继续获取G,优先从本地的P获取G

7.3.(场景3-4-5)

场景3:G2开辟过多的G

 场景4:G2本地满,再创建G7

        1. 将本地队列P拆分成2段

        2. 将 前一段和G7 打散,再存放在本地队列中

        

场景5:G2本地未满,创建G8:直接将G8放到本地队列中

        

7.4.(场景6)唤醒正在休眠的M

        M1与P1绑定,P1获取了G,此时,G2创建了G8

        

G2创建一个协程G8的时候

1. 首先尝试去休眠线程队列中,唤醒一个休眠的线程

2. 唤醒之后,将M从休眠线程队列中取出来

 

3. 此时,被唤醒的M2,将尝试与新的P绑定

        一旦M2绑定了空闲的P,此时会调用G0

        自旋线程M2的本地队列P2中没有G && M2正在运行G0去寻找G

        

7.5.(场景7)被唤醒的M2从全局队列中获取批量G

        获取G的个数 N = min{ len(GQ)/GOMAXPROCS+1, len(GQ/2) } , GQ:全局队列的总长度

        

7.6.(场景8)M2从M1中批量偷取G

假设此时全局队列中没有G,M2就需要从其他M的P中获取G(批量个数N=后半段)

        

7.7.(场景9)自旋线程的最大限制

        自旋线程 + 执行线程 <= GOMAXPROCS

        

        此时,假设新创建了M5,因为GOMAXPROCS=4,不能在创建自旋线程了,所以,M5会被放入休眠线程队列1

7.8.(场景10)G发生系统调用/阻塞

1. M2的P2执行G8,此时G8执行了systemcall 阻塞(此时M2绑定了G8)

 2. 因为此时M2的P2中存在G9,因为M2已经全权为G8负责了,为了不能阻塞G9的运行,所以P2会重新寻找有没有其他的M能继续为它执行(根据休眠线程队列中是否有空闲线程,分为两种情况)

        2.1. 有M

                P2将从空闲线程队列中取出M5,将P2挂到M5上(M5和P2组成新的MP)

                 

        2.2. 无M:将P放入空闲队列

                

7.9.(场景11)G发生系统阻塞,再变为非阻塞

        

        M2中的G8,此时变为非阻塞,执行过程见下

        1. M2中记录了上一次绑定的P,P是P2,即优先获取原配

                

         2. M2发现P2已经被绑定给了M5,因此,M2是抢不过M5的

                

        3. M2会先尝试从空闲P队列中寻找P

                

        4.空闲P队列没有P,此时M2放弃绑定P,将执行释放逻辑:① M2放到空闲线程队列 ②G8放到全局P队列

        

8. Golang系统调用与阻塞处理 😄

8.1. 阻塞

8.1.1. Go阻塞的4种场景

  1. 由于原子、互斥量、通道操作调用导致 Goroutine 阻塞,调度器将把当前阻塞的 Goroutine 切换出去,重新调度 本地P队列 上的其他 Goroutine
  2. 由于 网络请求、网络IO 操作导致 Goroutine 阻塞。Go 程序提供了网络轮询器(NetPoller)来处理网络请求和 IO 操作的问题,其后台通过 kqueue(MacOS),epoll(Linux)或 iocp 来实现 IO 多路复用。通过 使用 NetPoller 进行网络系统调用,调度器可以防止 Goroutine 在进行这些系统调用时阻塞 M。这可以让 M 执行 P 的 LRQ 中其他的 Goroutines,而不需要创建新的 M。执行网络系统调用不需要额外的 M,网络轮询器使用系统线程,它时刻处理一个有效的事件循环,有助于减少操作系统上的调度负载。用户层眼中看到的 Goroutine 中的“block socket”,实现了 goroutine-per-connection 简单的网络编程模式。实际上是通过 Go runtime 中的 netpoller 通过 Non-block socket + I/O 多路复用机制“模拟”出来的。
  3. 当调用一些系统方法的时候(如文件 I/O),如果系统方法调用的时候发生阻塞,这种情况下,网络轮询器(NetPoller)无法使用,而进行系统调用的 G1 将阻塞当前 M1。调度器引入 其它M 来服务 M1 的P。
  4. 如果在 Goroutine 去执行一个 sleep 操作,导致 M 被阻塞了。Go 程序后台有一个监控线程 sysmon,它监控那些长时间运行的 G 任务然后设置可以强占的标识符,别的 Goroutine 就可以抢先进来执行。

8.2. 系统调用与调度机制

8.2.1.异步系统调用

异步系统调用:网络IO

结论:当G1执行异步系统调用时,会发生阻塞,该阻塞动作,①不需要创建新的M,② G会和MP分离(G挂到netpoller),阻塞事件会由NetPoller接管

刚开始,G1在M上运行,此时G1想去执行「网络系统调用」

G1执行「网络系统调用」后,发生阻塞,此时,将G1挂在到NetPoller上&&监听G1网络系统调用的返回,M会从P队列中找到新的协程运行。(注:不需要创建新的M)

当G1的「网络系统调用」返回后,G1会被移回到P队列中

8.2.2.同步系统调用

同步系统调用:读写文件

结论:当G1执行同步系统调用时,G2会发生阻塞,同时会导致与G1绑定的M1也阻塞,之后,MG 会和P分离(P另寻M),当M从系统调用返回时,不会继续执行,而是将G放到run queue

刚开始,G1在M上运行,此时G1想去执行「同步系统调用」,G1会阻塞

同步调用,当G1阻塞后,会导致M1也阻塞,具体的执行动作是:G1和M1绑定在一起&&陷入阻塞,M1绑定的P会转移给新的M

阻塞的系统调用完成后:G1可以移回 LRQ 并再次由P执行。如果这种情况需要再次发生,M1将被放在旁边以备将来使用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值