调度器的自动分配和任务抢占

举报
码乐 发表于 2025/10/10 08:52:27 2025/10/10
【摘要】 1 简介运行时有多种同步机制。它们在语义不同,特别是在它们是否与goroutine调度器或操作系统调度器进行交互。最简单的是 “mutex”,它是通过 "lock "和 "unlock "来操作的。unlock来操作。这应该被用来保护短期内的共享结构时间。在mutex上的阻塞直接阻塞M,而不与Go调度程序交互。调度器(Scheduler)是 Go 能在高并发、高性能、低复杂度下运行的核心原...

1 简介

运行时有多种同步机制。它们在语义不同,特别是在它们是否与goroutine调度器或操作系统调度器进行交互。

最简单的是 “mutex”,它是通过 "lock "和 "unlock "来操作的。unlock来操作。
这应该被用来保护短期内的共享结构时间。在mutex上的阻塞直接阻塞M,而不与Go调度程序交互。

调度器(Scheduler)是 Go 能在高并发、高性能、低复杂度下运行的核心原因之一。

2、Go 调度器的核心思想:M:N 模型

Go 的调度器是一个用户态轻量级调度系统,采用 M:N 模型:

N 个 goroutine(G)通过调度器(S)映射到 M 个操作系统线程(M)。

也就是说:

不同于 OS 直接调度线程;

Go 在用户态自己调度 goroutine 到 系统线程;

避免了系统级上下文切换的高成本。

  • Go 调度器的三个关键结构

G(Goroutine) 轻量级执行单元(每个 go func) “任务”
M(Machine) 真实的操作系统线程 “工人”
P(Processor) 执行上下文,保存可运行的 G 队列 “流水线”

调度器核心关系:

			Goroutine (G)  -->  Processor (P)  -->  Machine (M)

例如:

    G1,G2,G3,...  →  P1 → M1 (绑定执行)
    G4,G5,G6,...  →  P2 → M2

3、Go 调度器算法关键特征

抢占式调度(Preemptive)

从 Go 1.14 开始,goroutine 可被抢占;

避免单个 goroutine 长时间占用 CPU。

工作窃取(Work Stealing)

每个 P 有自己的本地运行队列;

当 P 的队列空时,它会去其他 P 窃取一半的 goroutine 任务;

保证负载均衡。

协作式让出(Cooperative Yield)

调用 runtime.Gosched() 或 IO 阻塞时,goroutine 主动让出执行权。

系统监控线程 (sysmon)

负责清理定时器、触发 GC、检测长时间运行的 goroutine 等。

  • 代码示例:Go 调度器在并发执行中的协调

示例 1:多个 goroutine 并发运行,由调度器自动分配

    package main

    import (
        "fmt"
        "runtime"
        "time"
    )

    func task(id int) {
        for i := 0; i < 3; i++ {
            fmt.Printf("Task %d running on thread %d\n", id, getThreadID())
            time.Sleep(10 * time.Millisecond)
        }
    }

    func getThreadID() int {
        return runtime.Getg().M().ID // Go 1.23+ 可以使用 runtime.Getg() 调试
    }

    func main() {
        runtime.GOMAXPROCS(2) // 设置最多使用 2 个 CPU 线程

        for i := 1; i <= 5; i++ {
            go task(i)
        }

        time.Sleep(time.Second)
    }

GOMAXPROCS(2) 表示最多启用 2 个 P(处理器);

5 个 goroutine(G)会被调度器分配到 2 个 OS 线程(M)上轮流执行;

输出中你会看到任务在不同线程间切换。

(注:runtime.Getg() 仅在实验或调试模式可用;普通用户可使用 runtime.LockOSThread() 验证绑定。)

  • 示例 2:展示调度器的“任务抢占”

      package main
    
      import (
          "fmt"
          "runtime"
          "sync"
          "time"
      )
    
      func worker(id int, wg *sync.WaitGroup) {
          defer wg.Done()
          for i := 0; i < 5; i++ {
              fmt.Printf("Worker %d iteration %d on thread %d\n", id, i, getThreadID())
              time.Sleep(time.Millisecond * 50)
          }
      }
    
      func getThreadID() int {
          return runtime.Getg().M().ID
      }
    
      func main() {
          runtime.GOMAXPROCS(4) // 4个P
          var wg sync.WaitGroup
    
          for i := 1; i <= 8; i++ {
              wg.Add(1)
              go worker(i, &wg)
          }
    
          wg.Wait()
      }
    

调度器行为:

goroutine 分配到多个 P;

当某个 P 的 goroutine 执行完毕,空闲 P 会“偷取”别的 P 的 goroutine;

因此任务分布自动均衡,无需人工干预。

Go 通道简化了代码逻辑,避免竞争条件。

4 小结

go程序运行时总是与Go调度器进行交互。这意味着它可以安全地从这意味着它可以在运行时的最底层安全使用,但也会阻止任何相关的G和P被重新调度。

要直接与goroutine调度器交互,请使用goparkgoreadygopark停放当前的轮询程序,将其置于处于 "等待 "状态,并将其从调度器的运行队列中移出,并在在当前M/P上安排另一个goroutine。

goready将一个将停放的程序放回 "可运行 "状态,并将其加入运行队列中。

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。