go的并发模型

1. 概述

GMP 的基本概念

GMP 模型是 Go 语言中用于实现高效并发的机制。它主要涉及三个核心概念:Goroutines(G)、Machine(M)和 Processor(P)。这些组件的设计目的是高效地调度和执行大量的 Goroutines。

  • G (Goroutine)

    • 定义: Goroutine 是 Go 语言中的一个轻量级线程。它由 Go 运行时管理,能够并发执行任务。

    • 特点:

      • 轻量级: 创建和销毁开销较小,通常内存占用较少。

      • 栈管理: Goroutine 使用动态增长的栈,初始栈小,能够根据需要增长。

      • 调度: 调度器决定哪些 Goroutine 在何时运行。

  • M (Machine)

    • 定义: Machine 是与操作系统线程(OS线程)相关联的结构体,负责实际执行 Goroutines。

    • 特点:

      • 与系统线程关联: 每个 M 对应一个系统线程,用于执行 Goroutines。

      • 管理: M 在 Go 运行时中负责具体的执行任务。

  • P (Processor)

    • 定义: Processor 是处理器,负责管理和调度 Goroutines 的执行。

    • 特点:

      • 管理 Goroutines: 每个 P 维护一个 Goroutine 队列,调度这些 Goroutines 执行。

      • 与 M 关联: P 与 M 通过工作窃取等机制进行协作,以平衡负载。

GMP 的目标:

  • 高效调度: 高效地将大量的 Goroutines 映射到有限的系统线程上。

  • 优化性能: 减少线程上下文切换的开销,提高并发执行的效率。

Go 的并发模型简介

Go 的并发模型基于 CSP(Communicating Sequential Processes)理论,强调通过通信来实现并发操作。主要机制包括:

  • Goroutines:

    • 创建 : 使用 go 关键字创建新的 Goroutine。

    • 执行: Goroutine 可以并发执行不同的代码块,互不干扰。

    • 调度: Go 运行时调度器负责 Goroutines 的执行和管理。

  • Channels:

    • 定义: Channels 是用于 Goroutines 之间传递数据的管道。

    • 特点:

      • 类型安全: Channel 传递特定类型的数据,确保类型安全。

      • 同步: Channels 支持同步操作,确保数据传输的一致性。

    • 使用: Goroutines 可以通过 Channels 发送和接收数据,实现数据传递和同步。

  • Select:

    • 定义 : select 语句用于从多个 Channel 中选择一个进行操作。

    • 特点:

      • 多路复用 : select 可以处理多个 Channel 的读写操作。

      • 阻塞和非阻塞: 支持阻塞和非阻塞操作,根据条件选择执行路径。

Go 的并发模型的优势:

  • 简洁性: 使用 Goroutines 和 Channels 使并发编程更加简洁。

  • 灵活性 : select 语句提供了处理复杂并发场景的灵活性。

  • 性能: 高效的调度和管理机制,减少了上下文切换的开销。

图示

下面是展示 Go 并发模型的 Mermaid 图示:

  • Goroutines: 通过 Channel 进行数据通信。

  • Channel: 提供 Goroutines 之间的同步和数据传递机制。

  • Processor: 管理和调度 Goroutines。

  • Machine: 实际执行 Goroutines。

2. Goroutine

Goroutine 的定义和特点
  • 定义: Goroutine 是 Go 语言中的轻量级线程,是并发执行的基本单元。它由 Go 运行时(runtime)管理,可以与其他 Goroutines 并发地执行代码。

  • 特点:

    • 轻量级: Goroutine 的创建和销毁开销非常小,内存占用也较少。与操作系统线程相比,它们的资源消耗更低。

    • 栈动态增长: 每个 Goroutine 具有一个独立的栈,栈的初始大小较小,并且可以根据需要动态增长,这减少了内存的占用。

    • 调度透明: Goroutine 的调度是由 Go 运行时自动处理的,程序员不需要手动管理线程的调度和同步。

    • 并发性: 通过 Goroutines 可以实现并发处理,这使得 Go 程序能够高效地执行多个任务。

Goroutine 的创建和调度
  • 创建 : 使用 go 关键字来创建一个新的 Goroutine。go 关键字会将函数调用转换为异步执行的 Goroutine。

  • 调度:

    • Go 运行时调度器: Go 运行时中的调度器负责将 Goroutines 分配给系统线程(M)。调度器会使用时间片轮转算法和工作窃取算法来管理 Goroutines 的执行。

    • 调度策略:

      • 协作式调度 : Goroutines 可以主动让出 CPU 时间片,通过 runtime.Gosched() 函数来实现。

      • 抢占式调度: Go 运行时会周期性地抢占 Goroutines 以确保公平性,尤其是在执行长时间运行的操作时。

Goroutine 的栈管理
  • 初始栈: 每个 Goroutine 在创建时分配一个小的栈,通常是 2KB。

  • 动态增长:

    • 增长机制: 当 Goroutine 的栈空间不足时,Go 运行时会自动扩展栈。扩展过程包括将栈内容复制到一个新的、更大的内存块中。

    • 栈缩减: 当 Goroutine 的栈空间使用减少时,Go 运行时会尝试缩减栈的大小,以回收不再需要的内存。

Goroutine 的生命周期
  • 创建: Goroutine 被创建并添加到运行队列中,等待调度器分配到一个系统线程(M)上执行。

  • 运行: Goroutine 处于运行状态时,它正在系统线程上执行代码。此时,它可以与其他 Goroutines 并发执行。

  • 等待: 当 Goroutine 阻塞(例如,等待从 Channel 接收数据)或执行 IO 操作时,它会被放入等待队列中。

  • 终止: 当 Goroutine 执行完成或被显式终止时,它的生命周期结束。Go 运行时会清理相关的资源并将其从调度器中移除。

图示

下面是展示 Goroutine 栈管理和生命周期的 Mermaid 图示:

  • Create Goroutine: Goroutine 被创建并进入运行状态。

  • Running: Goroutine 在系统线程上运行。

  • Waiting: Goroutine 可能进入等待状态,直到满足条件继续执行。

  • Termination: Goroutine 执行完毕或被终止后进入终止状态。

  • Dynamic Stack Expansion: Goroutine 的栈在需要时扩展。

  • Stack Shrinkage: Goroutine 的栈在空间使用减少时缩小。

3. Scheduler

Scheduler 的作用

Go 调度器负责管理和调度 Goroutines 的执行,旨在高效地将 Goroutines 分配到有限的系统线程(M)上。它的主要作用包括:

  • 调度: 确定哪些 Goroutines 在何时运行,并将其分配到可用的系统线程上。

  • 负载均衡: 在多个处理器和系统线程之间分配 Goroutines,以确保系统资源的高效使用。

  • 并发控制: 处理 Goroutines 的同步和互斥,以避免竞态条件和提高并发性能。

  • 资源管理: 动态管理 Goroutines 的执行和栈的扩展/缩减。

M(Machine)、P(Processor)、G(Goroutine)模型
  • M (Machine)

    • 定义: M 是与系统线程关联的结构体,负责执行 Goroutines。

    • 特点: 每个 M 对应一个操作系统线程,它将执行被调度的 Goroutines。

  • P (Processor)

    • 定义: P 是一个逻辑处理器,负责管理和调度 Goroutines。

    • 特点 : 每个 P 维护一个 Goroutine 队列,调度这些 Goroutines 执行。P 的数量可以通过环境变量 GOMAXPROCS 配置。

  • G (Goroutine)

    • 定义: G 是 Go 语言中的执行单元,由调度器分配到 M 上执行。

    • 特点: Goroutine 是轻量级的线程,与系统线程相独立。

GMP 模型图示:

Scheduler 的工作原理
  • 调度过程:

    • Goroutines 队列: Goroutines 被放入待执行的队列中,由调度器管理。

    • 分配 P: 每个 P 维护一个 Goroutine 队列,调度器将 Goroutines 分配到 P 上。

    • 分配 M: P 将 Goroutines 分配给 M 执行。M 是实际执行 Goroutines 的系统线程。

    • 执行: M 执行被分配的 Goroutines,P 负责调度和管理这些 Goroutines 的执行。

  • 时间片轮转:

    • 调度器使用时间片轮转算法,确保每个 Goroutine 都能获得 CPU 时间片。调度器会定期切换正在运行的 Goroutines,以确保公平性。
  • 工作窃取:

    • 当一个 P 的 Goroutine 队列为空时,它可以从其他 P 的队列中窃取任务,以平衡负载。

Scheduler 工作原理图示:

线程与 Goroutine 的映射
  • 线程映射:

    • Go 运行时将多个 Goroutines 映射到少量的系统线程上。每个系统线程(M)可以同时执行多个 Goroutines。
  • 调度策略:

    • 协作式调度: Goroutines 可以主动让出 CPU 时间片。

    • 抢占式调度: Go 运行时定期抢占 Goroutines,以确保公平性和响应性。

Scheduler 的 负载均衡
  • 负载均衡 机制:

    • 工作窃取: 调度器允许 P 之间的工作窃取,以平衡负载。一个 P 在其队列空闲时,会尝试从其他 P 的队列中窃取任务。

    • 动态调整: 根据 Goroutines 的执行和等待状态,调度器动态调整 M 和 P 的分配,以确保系统资源的高效利用。

负载均衡 图示:

  • 任务窃取: 一个处理器(P)从另一个处理器(P)的任务队列中窃取任务,以平衡负载。

  • 动态调整: 调度器根据当前负载情况动态调整 Goroutines 的调度和执行策略。

4. M(Machine)

M 的定义和功能
  • 定义: M(Machine)是 Go 运行时中与操作系统线程直接关联的实体,负责执行 Goroutines。每个 M 对应一个系统线程。

  • 功能:

    • 执行 Goroutines: M 负责在其关联的系统线程上执行分配给它的 Goroutines。M 可以执行来自不同 P 的 Goroutines。

    • 系统调用处理: M 可以处理 Goroutines 中的阻塞操作,如系统调用(例如文件 I/O、网络 I/O 等),而不会阻塞其他 M 的执行。

    • 管理栈: M 负责管理 Goroutines 的栈,包括栈的增长和缩减。

    • 调度与管理: M 与 P 协同工作,执行由 P 分配的 Goroutines,并通过调度器实现高效的任务处理。

M 的创建和销毁
  • 创建:

    • 自动创建: 当有足够多的 Goroutines 需要执行且现有的 M 无法处理时,Go 运行时会自动创建新的 M。新的 M 会关联到一个新的操作系统线程。

    • 栈初始化: M 在创建时分配一定大小的栈空间,用于执行 Goroutines。栈空间可以根据需要动态扩展。

  • 销毁:

    • 自动销毁: 当一个 M 长时间没有任何可执行的 Goroutines 时,Go 运行时会回收该 M 所占用的资源,销毁该 M 以及它关联的操作系统线程。

    • 资源回收: M 销毁时,会释放它所占用的所有资源,包括关联的系统线程和栈空间。

M 创建与销毁的图示:

  • Create M: 根据需求创建新的 M。

  • Associate with OS Thread: 将 M 与一个操作系统线程关联。

  • Execute Goroutines: M 执行分配的 Goroutines。

  • Destroy M: 当 M 无任务可执行时,销毁 M 及其资源。

M 与系统线程的关系
  • 一对一关系: 每个 M 与一个操作系统线程直接关联。这意味着一个 M 只能在它所关联的线程上执行 Goroutines。

  • 独立执行: 每个 M 可以独立地执行 Goroutines,这与其他 M 并行运行。这种设计使得 Go 可以充分利用多核处理器,提高并发性能。

  • 阻塞处理:

    • 阻塞调用: 当一个 Goroutine 在 M 上进行阻塞调用(如 I/O 操作)时,该 M 可能会暂时停止调度其他 Goroutines,而其他 M 则继续执行不受影响的 Goroutines。

    • 避免阻塞影响: 为避免阻塞操作影响整体性能,Go 运行时可能创建新的 M 来处理未被阻塞的 Goroutines。

  • 系统线程调度:

    • 调度器控制: Go 调度器控制 M 与 P 之间的交互。当一个 M 被阻塞时,调度器可能会将 P 重新分配给另一个未被阻塞的 M 以继续执行 Goroutines。

    • 线程重用: Go 运行时会在 M 被销毁时重用操作系统线程,减少创建和销毁系统线程的开销。

M 与系统线程关系图示:

  • M1 和 T1: M1 与系统线程 T1 关联,M1 可能会因阻塞操作被阻塞。

  • M2 和 T2: M2 与 T2 关联,继续执行不受阻塞影响的 Goroutines。

  • M3 和 T3: 如果需要,可能创建新的 M3 以执行其他任务,并关联新的系统线程 T3。

5. P(Processor)

P 的定义和功能
  • 定义: P(Processor)是 Go 运行时中的一个逻辑处理器,它代表了运行时调度 Goroutines 的资源。P 是调度器和 M(Machine)之间的中间层,负责管理 Goroutines 的调度和执行。

  • 功能:

    • Goroutines 调度: P 维护一个本地的 Goroutine 队列,用于调度和分发 Goroutines 给 M(系统线程)执行。

    • 执行上下文管理: P 维护 Goroutines 的执行上下文,包括相关的栈和任务状态。

    • 系统资源管理: P 负责与 M 协作,将 Goroutines 分配给 M 来执行,并管理其与其他 P 之间的负载平衡。

    • 本地队列: 每个 P 维护一个本地 Goroutine 队列,优先调度该队列中的任务,减少跨线程调度的开销。

P 的调度策略
  • 本地调度:

    • 本地优先: 每个 P 维护一个本地的 Goroutine 队列,优先从本地队列中获取 Goroutines 进行调度,减少线程间的上下文切换和通信开销。

    • 工作窃取: 当一个 P 的本地队列空闲时,它可以从其他 P 的队列中窃取 Goroutines 以保持工作负载的平衡。这种策略称为工作窃取(Work Stealing)。

  • Goroutines 分发:

    • 分发给 M: P 将从本地队列中获取的 Goroutines 分配给关联的 M(系统线程)进行执行。如果 P 没有空闲的 M,可以创建新的 M 或从其他 P 窃取 M。

    • 上下文切换: 当 P 上的 M 执行完当前的 Goroutine 或需要进行阻塞操作时,P 可以选择切换上下文,将另一个 Goroutine 分配给 M 执行。

  • 负载均衡:

    • 动态调整: Go 调度器通过动态调整 P 的数量和每个 P 关联的 M,以确保 Goroutines 的负载均衡和系统资源的高效利用。

    • 减少阻塞影响: 当一个 P 的 Goroutines 队列阻塞时,调度器可能会重新分配 P 与其他 M 或创建新的 P 以继续执行其他任务。

P 调度策略图示:

  • P1 和 P2: 每个 P 维护自己的本地队列,优先调度本地队列中的 Goroutines。

  • Work Stealing: P2 当没有可执行任务时,从 P1 的队列中窃取任务以平衡负载。

P 的数量配置(GOMAXPROCS)
  • GOMAXPROCS 的定义 : GOMAXPROCS 是 Go 语言中的一个环境变量,用于配置 Go 程序可以使用的最大 P 数量,即逻辑 CPU 核心的数量。

  • 配置方式:

    • 默认值 : 默认情况下,GOMAXPROCS 的值等于机器的物理 CPU 核心数。这意味着,默认情况下,Go 程序可以并发运行与物理核心数相等的 P。

    • 手动设置 : 可以使用 runtime.GOMAXPROCS(n int) 函数来手动设置 GOMAXPROCS 的值,n 为要设置的 P 数量。这行代码将 P 的数量设置为 4。

  • 影响:

    • 并发度 : 设置 GOMAXPROCS 的值会直接影响 Go 程序的并发度。较高的 GOMAXPROCS 值可能提高并发执行效率,但也可能增加上下文切换的开销。

    • 负载均衡 : 如果 GOMAXPROCS 的值设置过低,可能会导致系统资源未充分利用;如果设置过高,则可能引发资源竞争和性能下降。

GOMAXPROCS 配置示意图:

  • GOMAXPROCS=2: 在此配置下,Go 程序使用两个 P,分别调度到 M1 和 M2 进行执行。

6. G(Goroutine)

G 的数据结构
  • 定义 : G(Goroutine)是 Go 语言中的轻量级线程,是并发执行的基本单位。每个 Goroutine 在 Go 运行时中都有一个对应的数据结构 G,用于保存其运行状态和相关信息。

  • G 数据结构:

    • stack: Goroutine 的栈空间,保存函数调用的局部变量和临时数据。栈的初始大小较小(通常是 2KB),但可以动态扩展,最大可以达到 1GB。

    • sched: 包含 Goroutine 调度相关的信息,如程序计数器(PC)、调用帧(SP)、状态(state)等,用于调度和恢复 Goroutine 的执行。

    • m: 当前 Goroutine 所属的 M(Machine),当 Goroutine 正在执行时,该字段指向其所在的 M 结构。

    • status: Goroutine 的当前状态,表示它是可运行、阻塞还是已经退出。

    • waitreason: 如果 Goroutine 处于等待状态,该字段指明等待的原因,如等待通道操作、锁、系统调用等。

    • entry: Goroutine 的入口函数,即该 Goroutine 所执行的函数。

G 数据结构示意图:

  • Stack: 存储 Goroutine 的栈帧。

  • Sched: 管理 Goroutine 的调度状态。

  • M: 指向执行该 Goroutine 的 M。

  • Status: 描述 Goroutine 的运行状态。

  • WaitReason: 如果 Goroutine 阻塞,指示阻塞原因。

  • Entry: Goroutine 执行的函数。

G 的调度与协作
  • 调度机制:

    • 协作式调度 : Go 运行时使用协作式调度(cooperative scheduling),这意味着 Goroutine 必须在适当的点主动让出 CPU,以便调度器可以将执行权交给其他 Goroutine。这些点通常是在进行 I/O 操作、通道通信或显式调用 runtime.Gosched() 时出现。

    • 抢占式调度: 为了防止 Goroutine 长时间占用 CPU,Go 运行时从 Go 1.14 开始引入了抢占式调度机制。这允许运行时在某些条件下(如 GC 开始时)强制中断一个长时间运行的 Goroutine,确保其他 Goroutine 能够获得执行机会。

  • Goroutine 的生命周期:

    • 创建 : Goroutine 通过 go 关键字创建,当创建一个新的 Goroutine 时,运行时会初始化一个 G 结构,并将其放入 P 的本地队列中。

    • 调度 : 调度器从 P 的本地队列中选择一个 G,将其分配给 M 执行。G 被 M 执行时,G 的 m 字段指向该 M。

    • 执行 : G 的 entry 函数被执行,运行时管理其栈帧和状态。执行过程中,G 可能会主动让出 CPU 或被抢占调度。

    • 休眠: 当 G 进行阻塞操作(如 I/O 或通道操作)时,G 的状态被标记为休眠,并将其从 M 上移除,放入等待队列。调度器会将 M 分配给其他可运行的 G。

    • 唤醒: 当阻塞操作完成后,G 被重新放入 P 的本地队列中,等待再次被调度执行。

    • 结束 : 当 G 的 entry 函数执行完毕,G 的状态被标记为已结束,运行时释放其栈和其他资源。

Goroutine 调度与协作示意图:

  • Create Goroutine: 创建新的 Goroutine 并将其放入 P 的队列。

  • Scheduler Selects G: 调度器选择 G 并分配给 M 执行。

  • Goroutine Yields or Blocks: 在合适的点上,Goroutine 主动让出或阻塞。

  • 协作调度示例:

    • I/O 操作: 在进行 I/O 操作时,Goroutine 会让出 CPU,并将其状态置为休眠。调度器将 M 分配给其他 Goroutine 执行,等待 I/O 操作完成后再唤醒原 Goroutine。
  • 抢占式调度示例:

    • 长时间运行的计算任务: 如果 Goroutine 在长时间计算任务中没有主动让出 CPU,运行时可能会在合适的点上强制抢占,切换到其他 Goroutine。

. GMP 之间的关系

M、P 和 G 的相互作用
  • M(Machine): 执行 Goroutines 的实际线程。每个 M 对应一个操作系统线程。

  • P(Processor): 管理 Goroutines 的调度。每个 P 维护一个本地的 Goroutine 队列,负责将 Goroutines 分配给 M 执行。

  • G(Goroutine): Go 的轻量级线程。G 是调度的基本单位,包含执行的函数和上下文信息。

M、P 和 G 的关系图示:

  • P1: 管理 Goroutines 的队列,分配给 M1 执行。

  • M1: 执行 G1 和 G2,M1 关联的操作系统线程处理这些 Goroutines。

  • G1 和 G2: 被 M1 执行的 Goroutines。

  • G3: 如果 P1 的队列中还有 G3,P1 将其分配给 M1 执行。

工作窃取与 负载均衡 机制
  • 工作窃取(Work Stealing):

    • 机制: 当一个 P 的本地队列空闲时,它可以从其他 P 的队列中窃取 Goroutines 以保持负载平衡。工作窃取是为了避免某些 P 因任务过少而闲置,同时其他 P 却过载的情况。

    • 实现: 工作窃取通常是通过在每个 P 中维护一个队列来实现的。空闲的 P 从其他 P 的队列尾部窃取任务,任务窃取的机制有助于分散负载,提高整体效率。

工作窃取机制图示:

  • P1、P2 和 P3: 多个 P 之间的工作窃取示意图。

  • 任务分布: 任务通过工作窃取机制从 P1 被分配到 P2 和 P3,确保任务均匀分配。

  • 负载均衡:

    • 动态调整: Go 运行时通过动态调整 P 和 M 的数量来实现负载均衡。调度器监控 Goroutines 的执行情况,并在需要时创建或销毁 P 和 M。

    • 减少阻塞: 当某些 Goroutines 阻塞时,调度器会重新分配 P 和 M,以减少阻塞对整体性能的影响。

线程切换与 Goroutine 切换
  • 线程切换:

    • 定义: 线程切换指的是操作系统在不同线程之间切换执行上下文的过程。线程切换通常涉及到保存和恢复线程状态、上下文信息等。

    • 开销: 线程切换的开销较大,因为涉及到操作系统层面的上下文切换和资源管理。

  • Goroutine 切换:

    • 定义: Goroutine 切换指的是 Go 运行时在不同 Goroutines 之间切换执行的过程。Goroutine 切换是协作式的,意味着 Goroutine 在合适的时机主动让出 CPU,或者在抢占式调度下由调度器强制切换。

    • 开销: Goroutine 切换的开销较小,因为不涉及操作系统级别的上下文切换。Go 运行时维护的 Goroutine 状态、栈帧等信息可以快速保存和恢复。

线程与 Goroutine 切换示意图:

  • Thread A、B 和 C: 操作系统级别的线程切换示意图。

  • Goroutine 1、2 和 3: Goroutine 切换示意图,显示 Goroutines 在同一个 M 上的切换过程。

8. 源码分析

Go 源码 中 GMP 相关的主要文件
  1. src/runtime/proc.go:

    1. 主要包含与 Goroutine(G)的调度和管理相关的代码。涉及 Goroutine 的创建、调度、挂起和恢复等操作。

    2. 关键函数: schedule(), gopark(), goready()

  2. src/runtime/sched.go:

    1. 包含调度器的主要实现,包括 M(Machine)和 P(Processor)的管理,以及 Goroutine 的调度策略。

    2. 关键函数: findrunnable(), schedule(), runtime·schedule()

  3. src/runtime/stack.go:

    1. 负责 Goroutine 的栈管理,包括栈的分配、扩展和切换。

    2. 关键函数: stackalloc(), stackswitch(), stackcopy()

  4. src/runtime/asm_amd64.s:

    1. 包含底层的汇编代码,用于实现 Goroutine 的上下文切换和栈切换等低级操作。

    2. 关键函数: gopreempt, gostart, gostart0

  5. src/runtime/proc.go:

    1. 负责操作系统线程(M)的管理和调度,包括创建、销毁、阻塞和唤醒 M。

    2. 关键函数: newm(), stopm(), findrunnable()

栈切换与调度的 源码 实现
  • 栈切换:

    • 栈切换是 Goroutine 切换过程中需要执行的操作。主要涉及到栈的保存和恢复。

    • stackswitch() : 在 src/runtime/stack.go 中实现,用于在不同的 Goroutine 之间切换栈帧。

    • stackalloc(): 负责分配新的栈空间,当 Goroutine 需要扩展栈时调用。

  • 栈切换示意图:

  • 调度:

    • 调度涉及到 Goroutine 的选择和分配,以及与 M 的协作。

    • schedule() : 在 src/runtime/proc.gosrc/runtime/sched.go 中实现,负责选择待运行的 Goroutine 并将其分配给 M 执行。

    • findrunnable() : 在 src/runtime/sched.go 中实现,查找可以运行的 Goroutine。

    • runtime·schedule(): 这是一个内部函数,用于调度 Goroutine 的执行。

  • 调度过程示意图:

Scheduler 的具体实现
  • 调度器结构:

    • Scheduler : 在 src/runtime/sched.go 中定义,负责管理所有的 P、M 和 Goroutines。

    • P: 处理 Goroutines 的调度,管理本地队列。

    • M: 实际执行 Goroutines 的线程。

    • G: Goroutine 本身。

  • 调度逻辑:

    • findrunnable(): 查找并选择一个可以运行的 Goroutine。如果没有找到,则可能会选择阻塞的 M 或创建新的 P。

    • schedule(): 将选择的 Goroutine 分配给 M 执行,并处理调度过程中的各种状态切换。

  • Scheduler 逻辑示意图:

相关的数据结构和算法
  • 数据结构:

    • G (Goroutine):

      • stack: Goroutine 的栈,存储局部变量和调用帧。

      • sched: 调度相关信息。

      • m: 当前的 M(Machine)。

      • status: Goroutine 的状态。

    • P (Processor):

      • runq: 本地的 Goroutine 队列。

      • m: 关联的 M 列表。

      • g: 本地管理的 Goroutine。

    • M (Machine):

      • g: 当前正在执行的 Goroutine。

      • status: M 的状态。

  • 算法:

    • 工作窃取算法: 用于在 P 之间平衡 Goroutine 的负载,减少某些 P 过载而其他 P 空闲的情况。

    • 调度算法: 决定如何选择和分配 Goroutines 给 M 执行,包括协作式和抢占式调度策略。

数据结构示意图:

  • Goroutine G: 包含栈和状态,执行时与 M 关联。

  • Processor P: 管理本地队列和 M。

  • Machine M: 执行 Goroutines。

9. 性能优化

GMP 机制的优化策略

优化 Go 的 GMP 机制主要集中在以下几个方面:

  1. Goroutine 调度优化:

    1. 减少调度开销: 通过优化调度器的实现,减少不必要的 Goroutine 切换和上下文切换开销。例如,减少频繁的全局锁竞争和改进调度策略,以提高调度效率。

    2. 批量调度: 调度器可以批量处理多个 Goroutines,减少上下文切换次数,提高调度效率。

    3. 改善调度策略: 通过调整调度策略,如优先级调度、动态调整 P 和 M 的数量等,来提高系统的整体性能。

  2. 栈管理优化:

    1. 栈扩展策略: 通过优化栈的扩展算法,减少栈的重新分配和复制开销。例如,使用更高效的栈扩展算法和减少频繁的栈扩展操作。

    2. 栈切换优化: 通过减少栈切换的频率和开销,优化 Goroutine 切换性能。

  3. 工作窃取与 负载均衡:

    1. 优化工作窃取算法: 通过改进工作窃取算法,减少不必要的工作窃取操作和提高负载均衡的效率。

    2. 动态调整 P 和 M: 根据系统负载动态调整 P 和 M 的数量,保持系统的负载平衡和资源利用率。

  4. 减少锁竞争:

    1. 减少全局锁: 优化全局锁的使用,减少锁竞争带来的性能瓶颈。例如,使用更细粒度的锁或无锁的数据结构来提高并发性能。

GMP 性能优化示意图:

影响性能的因素
  1. Goroutine 的数量:

    1. 过多的 Goroutine: 过多的 Goroutine 可能导致过高的调度开销和栈切换开销,影响整体性能。

    2. 合理数量的 Goroutine: 适当的 Goroutine 数量可以提高并发性能,减少调度和上下文切换的开销。

  2. 栈的大小和扩展:

    1. 栈扩展开销: 栈的频繁扩展可能带来性能问题,特别是在高并发场景下。

    2. 栈大小配置: 合理配置初始栈大小和扩展策略,可以减少栈扩展开销和提高性能。

  3. 调度器的效率:

    1. 调度算法: 调度器的效率直接影响 Goroutine 的切换性能和整体系统性能。

    2. 工作窃取效率: 工作窃取算法的效率决定了负载均衡的效果。

  4. 锁的使用:

    1. 锁竞争: 高频繁的锁竞争会导致性能瓶颈,特别是在高并发环境下。

    2. 锁优化: 减少全局锁的使用和优化锁的策略可以提高系统的性能。

如何调优 Goroutine 性能
  1. 分析和监控:

    1. 使用性能分析工具: 利用 Go 的性能分析工具(如 pprof)监控 Goroutine 的使用情况和调度性能,识别性能瓶颈。

    2. 定期性能测试: 定期进行性能测试和基准测试,以确保系统在高负载下的性能表现。

  2. 优化 Goroutine 创建:

    1. 避免过度创建 Goroutine: 在高并发场景中,避免过度创建 Goroutine,使用 Goroutine 池来复用 Goroutine。

    2. 控制 Goroutine 生命周期: 合理管理 Goroutine 的生命周期,避免长时间占用资源。

  3. 调整 GOMAXPROCS:

    1. 设置合适的 GOMAXPROCS: 根据系统的 CPU 核心数和负载情况,合理设置 GOMAXPROCS,以充分利用 CPU 资源。
  4. 减少锁和竞态:

    1. 优化锁策略: 使用更细粒度的锁或无锁数据结构,减少锁的竞争。

    2. 使用 sync 包: 使用 Go 的 sync 包中的同步原语(如 Mutex、WaitGroup)来优化并发操作。

  5. 优化栈使用:

    1. 配置栈大小: 根据实际需求合理配置初始栈大小,避免不必要的栈扩展。

    2. 使用栈扩展优化: 优化栈扩展的实现,减少栈扩展开销。

Goroutine 性能优化示意图:

  • Monitor and Profile: 使用 pprof 等工具监控性能。

  • Optimize Goroutine Creation: 通过重用 Goroutine 和控制生命周期来优化性能。

  • Adjust GOMAXPROCS: 根据 CPU 核心数和负载情况调整 GOMAXPROCS。

  • Reduce Lock Contention: 使用更细粒度的锁和优化锁策略。

  • Optimize Stack Usage: 合理配置栈大小和优化栈扩展。

10. 图示与总结

GMP 模型的图示
  1. GMP 模型整体结构:

    1. M(Machine): 执行 Goroutines 的操作系统线程。

    2. P(Processor): 管理 Goroutines 的调度,维护一个本地队列。

    3. G(Goroutine): Go 的轻量级线程,执行实际的任务。

  2. GMP 模型图示:

  1. Goroutine 切换与调度:

    1. 栈切换: Goroutine 切换时,涉及到栈的保存和恢复。

    2. 调度过程: Scheduler 选择一个待运行的 Goroutine,并将其分配给 M 执行。

  2. Goroutine 切换与调度图示:

关键点总结
  1. GMP 模型:

    1. G: Goroutine,是 Go 语言的轻量级线程,负责执行任务。

    2. P: Processor,负责管理 Goroutines 的调度和执行,维护本地队列。

    3. M: Machine,执行 Goroutines 的实际线程,通常对应一个操作系统线程。

  2. Scheduler:

    1. 调度器负责选择和分配 Goroutines 给 M 执行,采用工作窃取和负载均衡机制。

    2. 主要数据结构包括本地队列、全局队列和调度器状态。

  3. 栈管理:

    1. Goroutine 的栈通过扩展和切换来支持动态的内存需求。

    2. 栈的扩展和切换涉及到内存的分配和复制操作。

  4. 优化策略:

    1. 优化调度效率,减少不必要的上下文切换。

    2. 合理配置 Goroutine 的数量和栈大小。

    3. 使用高效的工作窃取算法和动态调整 P 和 M 的数量。

  5. 性能监控与调优:

    1. 使用性能分析工具(如 pprof)监控和分析 Goroutine 的性能。

    2. 控制 Goroutine 的创建,减少锁的竞争,优化栈管理。

常见问题及解答
  1. 如何处理 Goroutine 的大量创建问题?

    1. 解答: 避免在高并发场景中创建过多的 Goroutine。可以使用 Goroutine 池来复用 Goroutine,以减少资源消耗和调度开销。
  2. 为什么调度器会出现性能瓶颈?

    1. 解答: 调度器性能瓶颈可能是由于调度算法不够高效、频繁的全局锁竞争、或者 Goroutine 切换过于频繁。优化调度算法和减少锁的使用可以缓解这些问题。
  3. 如何提高 Goroutine 的栈切换效率?

    1. 解答: 优化栈的扩展算法,减少栈的频繁扩展和切换开销。合理配置初始栈大小,避免过多的栈切换。
  4. 工作窃取机制如何影响性能?

    1. 解答: 工作窃取机制可以平衡 Goroutine 的负载,提高系统的整体性能。优化工作窃取算法和减少不必要的工作窃取操作可以进一步提高性能。
  5. 如何根据系统负载调整 GOMAXPROCS?

    1. 解答: 根据系统的 CPU 核心数和负载情况,合理设置 GOMAXPROCS。可以通过性能监控工具来动态调整 GOMAXPROCS 以获得最佳性能。
相关推荐
docuxu1 分钟前
软考-信息安全-基础知识
网络·安全·web安全
草木·君3 分钟前
【SQL】百题计划:SQL排序Order by的使用。
数据库·sql
Ja_小浩12 分钟前
【计算机网络】socket编程 && 几个网络命令
服务器·网络·计算机网络
马丁的代码日记17 分钟前
深入剖析 Netty 中 TCP 粘包和拆包问题的解决之道
服务器·网络·tcp/ip
小白电脑技术31 分钟前
OpenWRT有三个地方设置DNS,究竟设置哪个地方会更好?
网络
好奇的菜鸟1 小时前
GORM安全-保护你的应用免受SQL注入攻击
数据库·sql·安全
盒马盒马1 小时前
MySQL:事务
数据库·mysql
insid1out1 小时前
python 连接 oracle 报错
数据库·oracle
码猩1 小时前
VBA 把Excel表当做一个大数据库来操作
数据库·excel
陈小唬1 小时前
树形结构构建的两种方式
java·数据库·算法