Go语言GMP 调度流程

发布于 1周前 作者 nodeper 最后一次编辑是 5天前 来自 问答

Go语言GMP指的是 G(Goroutine)、M(Machine)、P(Processor),Go语言GMP 调度流程如下:

gmp.png

每个 P 有个局部队列,局部队列保存待执行的 goroutine(流程 2),当 M 绑定的 P 的的局部队列已经满了之后就会把 goroutine 放到全局队列(流 程 2-1)

每个 P 和一个 M 绑定,M 是真正的执行 P 中 goroutine 的实体(流程 3), M 从绑定的 P 中的局部队列获取 G 来执行

当 M 绑定的 P 的局部队列为空时,M 会从全局队列获取到本地队列来执行 G (流程 3.1),当从全局队列中没有获取到可执行的 G 时候,M 会从其他 P的局部队列中偷取 G 来执行(流程 3.2),这种从其他 P 偷的方式称为 work stealing

当 G 因系统调用(syscall)阻塞时会阻塞 M,此时 P 会和 M 解绑即hand off,并寻找新的 idle 的 M,若没有 idle 的 M 就会新建一个 M(流程 5.1)

当 G 因 channel 或者 network I/O 阻塞时,不会阻塞 M,M 会寻找其他runnable 的 G;当阻塞的 G 恢复后会重新进入 runnable 进入 P 队列等待执 行(流程 5.3)

1 回复

在Go语言中,GMP(Goroutine, M(Machine,即线程或执行体), P(Processor,即处理器))模型是其并发执行的核心机制。这个模型有效地管理了Go程序中的并发执行单元(Goroutines)、执行这些Goroutines的线程(M)以及这些线程所依赖的处理器资源(P)。

GMP 调度流程简介

  1. Goroutine(G):Go语言的并发执行单元,比线程更轻量。
  2. Machine/Thread(M):操作系统线程,负责执行Goroutines。
  3. Processor(P):代表M执行G所需的资源,包括运行队列等。P的数量默认等于CPU核心数,但可调整。

调度流程

  1. 创建Goroutine(G):当使用go关键字时,会创建一个新的Goroutine。
  2. 分配Processor(P):新创建的Goroutine会被放入全局队列或某个P的本地队列中等待执行。
  3. 绑定Machine(M):当M创建或空闲时,它会尝试从全局队列、P的本地队列或网络轮询中获取G来执行。
  4. 执行Goroutine:M执行G中的代码。
  5. Goroutine阻塞与唤醒:如果G在执行过程中需要等待I/O、锁等,它会被阻塞,并从M上取下,M会尝试执行其他G。当G的条件满足时(如I/O完成),它会被唤醒并重新加入执行队列。
  6. Goroutine结束:G执行完毕后,M会尝试从队列中获取新的G来执行,如果没有则M可能进入休眠状态。

示例代码(概念性)

虽然GMP的调度是Go运行时内部实现的,但可以通过Go代码感受其并发特性:

package main

import (
    "fmt"
    "runtime"
    "sync"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Printf("Goroutine %d is running on P: %d\n", id, runtime.GOMAXPROCS(0)) // 注意:这里只是示意,实际获取当前P的ID需要更复杂的逻辑
    // 模拟工作
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go worker(i, &wg)
    }
    wg.Wait()
    fmt.Println("All goroutines completed.")
}

注意:上述代码中的runtime.GOMAXPROCS(0)只是用来获取当前P的数量(即CPU核心数,或用户通过runtime.GOMAXPROCS设置的数量),并不是直接获取当前Goroutine运行的P的ID。

GMP调度器的具体实现细节和调度策略(如工作窃取、系统调用返回时的G调度等)是复杂的,并且可能随着Go版本的更新而变化。

回到顶部