调度器的自动分配和任务抢占
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调度器交互,请使用gopark
和goready
。gopark
停放当前的轮询程序,将其置于处于 "等待 "状态,并将其从调度器的运行队列中移出,并在在当前M/P上安排另一个goroutine。
goready
将一个将停放的程序放回 "可运行 "状态,并将其加入运行队列中。
- 点赞
- 收藏
- 关注作者
评论(0)