深入解析Golang GMP

文章目录

1. 引言

Go 语言自发布以来,以其轻量级的并发处理能力备受开发者的青睐。在传统的并发编程模型中,创建和管理线程往往是高成本且复杂的操作。而 Go 语言通过引入 GoroutineGMP 调度模型,以一种高效且低开销的方式管理并发任务,使得并发编程变得更加简洁易用。

GMP 模型是 Go 语言并发的核心调度机制,它由三个关键组件构成:G(Goroutine)、M(Machine,即操作系统线程)和 P(Processor)。这些组件协同工作,确保 Goroutine 能够高效地在多个内核上调度执行。相比传统的线程模型,Go 语言的 GMP 模型具有更高的并发性能和调度灵活性。

在本文中,我们将通过对 Go 1.19 版本的源码进行深入解析,详细剖析 GMP 模型及其调度机制的内部实现。本文旨在帮助开发者理解 Go 并发调度的核心原理,并通过具体代码示例,展示 Go 调度器是如何高效地管理 Goroutine 的执行和调度的。


2. GMP 模型概述与核心结构体

在 Go 语言的调度模型中,GMP 是实现并发调度的三大核心组件:G(Goroutine)、M(Machine)和 P(Processor)。它们通过密切协作,确保 Goroutine 可以在多个线程和处理器上被有效地分配和执行。为了深入理解 GMP 模型的运作,我们需要从源码层面解析这三者的结构体及其关键字段。

在Go语言的GMP调度模型中,GMP 以及 schedt 是调度系统的核心结构体,它们分别代表了 Goroutine、操作系统线程和逻辑处理器等不同的角色。下面详细解析每个结构体的作用以及其中每个字段的功能。

2.1. G(Goroutine)

G 结构体表示 Go 语言中的 Goroutine。Goroutine 是 Go 语言中用于并发编程的最小调度单位。每个 Goroutine 都是轻量级的用户态线程,它是通过 G 结构体来管理的。

type g struct {
   
    // g 的执行栈空间
    stack       stack   
  
    // g 从属的 m
    m           *m      
    // g的状态
    atomicstatus uint32
}

字段解析:

  • stack: 这是 G 的执行栈,包含了这个 Goroutine 的所有函数调用栈帧和局部变量。Go 中的 Goroutine 使用的是可增长的栈,以支持 Goroutine 的轻量特性。

  • m: 表示该 G 当前从属的 M,即正在执行这个 Goroutine 的操作系统线程。G 是通过 M 来运行的,因此该字段指向了负责运行此 GM

  • atomicstatus: G 的状态,表示当前这个 Goroutine 的运行状态。这个字段是原子操作的,用来确保线程安全。不同的状态可能包括:正在运行、阻塞、等待、退出等。

2.2. M(Machine/Thread)

M 结构体表示 操作系统线程,即 M 负责实际执行 Goroutine 的调度。M 是物理上的执行单元,它与操作系统的线程一一对应。

type m struct{
   
    // 用于调度普通 g 的特殊 g
    g0      *g     

    // m 的唯一 id
    procid  uint64

    // 用于处理信号的特殊 g
    gsignal *g              

    // m 上当前运行的 g
    curg    *g       

    // m 关联的 p
    p       puintptr 
}

字段解析:

  • g0: M 负责调度多个 G,而 g0 是用于执行调度操作的特殊 Goroutine。g0 不参与用户代码的执行,而是处理调度、垃圾回收等系统任务。

  • procid: 该 M 的唯一 ID,通常对应于操作系统中的线程 ID,用于标识操作系统中的某个线程。

  • gsignal: gsignal 是用于处理系统信号的特殊 Goroutine。系统信号(如 SIGINTSIGTERM 等)会被 gsignal 捕获并处理。

  • curg: 表示当前 M 上正在运行的 G。每个 M 只能同时执行一个 Goroutine,这个字段保存的是当前正在运行的 G

  • p: 表示与 M 关联的 PM 需要通过 P 才能执行 Goroutine,这个字段指向当前 M 所拥有的 PP 是调度的逻辑处理器,每个 P 管理着待执行的 G 列表。

2.3. P(Processor)

P 结构体代表 逻辑处理器,是调度系统中的核心部分。每个 P 负责管理一组待执行的 GoroutinePM 结合后,M 才能运行 G

type p struct {
   
    id          int32

    status      uint32
    
    // p 所关联的 m. 若 p 为 idle 状态,可能为 nil
    m           muintptr   // back-link to associated m (nil if idle)

    // lrq 的队首
    runqhead    uint32
    // lrq 的队尾
    runqtail    uint32
    // q 的本地 g 队列——lrq
    runq        [256]guintptr
    // 下一个调度的 g. 可以理解为 lrq 中的特等席
    runnext     guintptr
}

字段解析:

  • id: P 的唯一标识符,通常用于标识系统中的某个逻辑处理器。

  • status: P 的当前状态。不同状态可以是:

    • _Pidle: 0 表示 P 处于空闲状态,没有 M 运行。
    • _Prunning: 1 表示 P 正在运行某个 G,由 M 所持有。
    • _Psyscall: 2 表示 P 正在执行系统调用,此时可能会被抢占。
    • _Pdead: 4 表示 P 已被终止。
  • m: 表示与 P 关联的 M。当 P 处于运行状态时,这个字段指向当前正在执行的 M。如果 P 是空闲状态,m 可能为 nil

  • runqhead/runqtail: 这两个字段表示 P 的本地运行队列 runq 的队首和队尾索引,runq 存放着待调度执行的 G

  • runq: P 的本地队列,用于保存待执行的 Goroutine。这个队列是每个 P 自己的局部队列,与其他 P 独立。

  • runnext: 用于标记下一个将被优先调度的 GrunnextP 的特殊位置,可以理解为具有优先级的 Goroutine。

2.4. 全局调度器schedt(Scheduler)

schedt全局调度器,负责管理所有的 GMP。它存储了空闲的 PM 以及全局队列中的 G

type schedt struct {
   
    // 锁
    lock mutex

    // 空闲 m 队列
    midle        muintptr 
	
    // 空闲 p 队列
    pidle        puintptr 

    // 全局 g 队列——grq
    runq         gQueue
    
    // grq 中存量 g 的个数
    runqsize     int32
}

字段解析:

  • lock: 全局调度器的锁,用于保证调度数据结构的并发安全。在多线程环境下,对 schedt 的修改需要通过 lock 来保证。

  • midle: 空闲的 M 列表。系统中未执行任务的 M 会被放入该队列中,当需要调度新的 G 时,从该队列中取出 M

  • pidle: 空闲的 P 列表。没有正在执行 GP 处于空闲状态时会进入这个队列,调度器会从中选择 PM 绑定以执行 G

  • runq: 全局 Goroutine 队列(grq),当所有 P 的本地队列都满时,G 会被放入全局队列。全局队列中的 G 可以被任意 P 窃取(work stealing)。

  • runqsize: 全局队列 runq 中的 G 数量,用于记录目前全局等待执行的 G 个数。


这些结构体与字段的设计使得 Go 的 GMP 调度系统能够有效地调度大量 Goroutine,确保程序在多核处理器上高效运行。

3. Goroutine 的生命周期与状态管理

Goroutine 的状态管理是 Go 语言调度器工作的核心。Go 调度器通过对 Goroutine 不同状态的管理,来决定何时执行、挂起或销毁一个 Goroutine。下面是 Goroutine 生命周期中涉及的核心状态,它们定义了每个 Goroutine 在不同阶段的具体情况。

3.1 Goroutine 的核心状态列表

Goroutine 的状态在 Go 源码中的定义如下:

const (
    _Gidle       = iota // 0 空闲状态
    _Grunnable          // 1 可运行状态
    _Grunning           // 2 正在运行状态
    _Gsyscall           // 3 系统调用中
    _Gwaiting           // 4 等待状态
    _Gdead              // 6 已结束状态
    _Gcopystack         // 8 复制栈中
    _Gpreempted         // 9 被抢占状态
)

每个状态代表了 Goroutine 在其生命周期中的不同阶段,调度器通过这些状态管理 Goroutine 的调度、挂起和销毁。

3.2 各个状态的详细解析

  1. _Gidle(空闲状态,值为 0):
    Goroutine 处于空闲状态,尚未被初始化。处于该状态的 Goroutine 还没有被调度器分配任何工作,通常是新创建的 Goroutine 或者已经结束的 Goroutine 被重新分配资源之前的状态。

  2. _Grunnable(可运行状态,值为 1):
    Goroutine 已经准备好,可以被调度器选中执行。处于该状态的 Goroutine 会被放入 P 的本地运行队列或者全局队列中,等待 M 线程分配 CPU 资源进行执行。

  3. _Grunning(正在运行状态,值为 2):
    Goroutine 正在被某个 M(操作系统线程)执行。当 Goroutine 被从 P 的任务队列中取出后,调度器会将其状态设置为 _Grunning,表示该 Goroutine 正在占用 CPU 资源。

  4. _Gsyscall(系统调用中,值为 3):
    Goroutine 进入了系统调用(如文件 I/O、网络操作等)状态,此时 Goroutine 并不会占用 CPU 资源,但它暂时无法继续执行。系统调用完成后,Goroutine 会从 _Gsyscall 状态转换为 _Grunnable,等待调度器再次调度。

  5. _Gwaiting(等待状态,值为 4):
    Goroutine 因等待某个事件(如通道操作、锁、定时器等)而处于阻塞状态。在此状态下,Goroutine 不会消耗 CPU 资源,只有当等待条件满足时,调度器才会将其状态切换回 _Grunnable,准备再次调度执行。

  6. _Gdead(已结束状态,值为 6):
    Goroutine 已经执行完毕,并且不再需要被调度执行。处于 _Gdead 状态的 Goroutine 不会被调度器选中,最终会被垃圾回收器回收。

  7. _Gcopystack(复制栈中,值为 8):
    Goroutine 正在进行栈空间的扩展或收缩操作。由于 Goroutine 的栈是可动态调整大小的,在某些情况下,调度器需要将 Goroutine 的栈从一块内存区域复制到另一块更大或更小的区域。这个状态表示 Goroutine 正处于这种栈调整的过程中。

  8. _Gpreempted(被抢占状态,值为 9):
    Goroutine 被抢占,暂停执行。Go 调度器为了防止长时间运行的 Goroutine 占用过多的 CPU 资源,会在适当时机主动抢占 Goroutine 的执行。被抢占的 Goroutine 会从 _Grunning 状态切换到 _Gpreempted,等待再次被调度。

3.3 Goroutine 状态的转换过程

Goroutine 在其生命周期中不断在不同状态之间进行转换。以下是各个状态之间的转换逻辑及其触发条件:

  1. _Gidle_Grunnable
    Goroutine 在被创建之后会从空闲状态 _Gidle 切换到 _Grunnable,准备好被调度执行。

    newg := new(g) // 创建新的 Goroutine
    newg.status = _Grunnable // 设置为可运行状态
    
  2. _Grunnable_Grunning
    当调度器选择一个可运行的 Goroutine 时,它的状态会从 _Grunnable 转变为 _Grunning,表示它正在被某个 M 执行。

    func execute(gp *g) {
         
        casgstatus(gp, _Grunnable, _Grunning) // 状态从可运行到正在运行
        run(gp) // 开始执行 Goroutine
    }
    
  3. _Grunning_Gwaiting
    当 Goroutine 等待某个外部条件(如通道操作、锁、定时器等)时,调度器会将其状态从 _Grunning 切换为 _Gwaiting,以释放 CPU 资源。

    casgstatus(gp, _Grunning, _Gwaiting) // 切换到等待状态
    
  4. _Gwaiting_Grunnable
    当等待条件满足后,Goroutine 会被唤醒,并重新进入 _Grunnable 状态,等待调度器再次分配执行机会。

    ready(gp) // 唤醒 Goroutine,状态切换为 _Grunnable
    
  5. _Grunning_Gsyscall
    当 Goroutine 进行系统调用(如文件或网络 I/O)时,状态会从 _Grunning 切换为 _Gsyscall,此时 Goroutine 不再消耗 CPU 资源,直到系统调用完成。

  6. _Grunning_Gdead
    当 Goroutine 执行完成时,它的状态会被设置为 _Gdead,表示该 Goroutine 已经结束,不再需要被调度。

    casgstatus(gp, _Grunning, _Gdead) // 设置为结束状态
    
  7. _Grunning_Gpreempted
    如果调度器决定抢占某个正在运行的 Goroutine,它的状态会从 _Grunning 切换到 _Gpreempted,等待下一次调度时再被执行。

    casgstatus(gp, _Grunning, _Gpreempted) // Goroutine 被抢占
    
  8. _Gcopystack 状态的特殊性:
    当 Goroutine 的栈空间不足,且需要扩展或收缩时,它会进入 _Gcopystack 状态。在此期间,调度器会将 Goroutine 的栈内容复制到一个新的内存区域,然后再将其状态切回 _Grunnable,以继续执行。

3.4 Goroutine 状态图

从 Goroutine 的生命周期来看,它们的状态转换可以概述为如下图示:

生命周期

4. G、M、P 的协作关系

Go 的 GMP 模型由三部分组成:Goroutine (G)Machine (M)Processor §。它们的协作构成了 Go 调度器的基础。在这一节中,我们将详细探讨 G、M、P 三者之间的协同工作机制,并结合源码解析它们如何共同完成高效的并发任务调度。

4.1 GMP 模型中的协同工作机制

在 Go 的并发模型中,G、M 和 P 的分工如下:

  • G(Goroutine): Goroutine 是 Go 语言中并发执行的最小单元,类似于其他编程语言中的线程或轻量级进程,但相比之下开销要小得多。Goroutine 被封装在 g 结构体中,调度器负责管理和调度这些 Goroutine 的执行。

  • M(Machine): M 代表操作系统的线程(OS thread),每个 M 都与一个或多个 Goroutine 关联。M 负责实际执行 Goroutine 中的任务。可以认为 M 是调度器在操作系统中的执行代理,它直接与系统内核进行交互。

  • P(Processor): P 是 Goroutine 的执行上下文。它持有本地的 Goroutine 运行队列,并与 M 进行协作,将 Goroutine 分配给 M 执行。P 的数量由 GOMAXPROCS 决定,表示可以同时执行 Goroutine 的最大核数。

协作关系的简要描述

  • Goroutine (G) 是执行单元,由 Processor § 管理,P 负责分配 Machine (M) 来运行 G。
  • Machine (M) 是物理上的线程,负责运行被 P 分配的 Goroutine。

简化来说,M 是线程,P 是调度的上下文,G 是具体要执行的任务。

4.2 G、M、P 的具体工作流程

G、M、P 三者协作的整体调度流程如下:

  1. Goroutine 创建: 当程序调用 go 关键字创建一个新的 Goroutine 时,新的 Goroutine 会被分配给当前的 P 并放入它的本地运行队列,处于 _Grunnable 状态,等待执行。

  2. M 执行 G: 每个 M 都会绑定一个 P,M 通过 P 的本地运行队列获取可运行的 Goroutine 并执行。P 将其本地队列中的 Goroutine 分配给 M 并切换 G 到 _Grunning 状态,M 负责实际运行 G。

  3. 任务完成或阻塞: 当 Goroutine 完成任务或者需要等待某个外部事件时(如 I/O、锁、信号等),它会进入 _Gwaiting 状态。M 会释放该 Goroutine,并继续从 P 的队列中取下一个 Goroutine 进行执行。

  4. 负载均衡: 当一个 P 的本地任务队列为空时,M 会尝试从全局任务队列或者其他 P 的本地队列中“窃取” Goroutine 任务执行。这种机制确保了所有 M 都能充分利用 CPU 资源,提高并发执行效率。

4.3 源码解析:G、M 和 P 之间的协作

在 GMP 模型中,M、P 和 G 是如何交互的?下面通过核心代码片段详细解析它们的协作关系。

4.3.1 G 的分配与执行

当一个新的 Goroutine 被创建时,它会被分配到当前 P 的本地运行队列中,P 会管理这些 Goroutine 并分配给 M 执行。P 的本地队列最多可以存储 256 个 Goroutine,如果队列已满,多余的 Goroutine 会被放入全局队列中。

func newproc1(fn *funcval, argp unsafe.Pointer, narg i
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值