文章精选推荐
1 JetBrains Ai assistant 编程工具让你的工作效率翻倍
2 Extra Icons:JetBrains IDE的图标增强神器
3 IDEA插件推荐-SequenceDiagram,自动生成时序图
4 BashSupport Pro 这个ides插件主要是用来干嘛的 ?
5 IDEA必装的插件:Spring Boot Helper的使用与功能特点
6 Ai assistant ,又是一个写代码神器
7 Cursor 设备ID修改器,你的Cursor又可以继续试用了
文章正文
Golang的调度器是其并发模型的核心组件,负责管理Goroutine的调度和执行。Goroutine是Go语言中的轻量级线程,由Go运行时(runtime)管理。
调度器的设计目标是高效地利用多核CPU,同时保持低延迟和高吞吐量。下面我们从理论和代码层面分析Golang调度器的关键机制。
1. 调度器的基本概念
1.1 Goroutine
Goroutine是Go语言中的并发执行单元,比操作系统线程更轻量。每个Goroutine初始时只占用几KB的栈空间,栈空间可以根据需要动态增长或缩减。
1.2 M-P-G 模型
Go调度器采用了M-P-G模型:
- M (Machine): 代表操作系统线程(OS Thread),由操作系统调度。
- P (Processor): 代表逻辑处理器,负责调度Goroutine。P的数量通常等于CPU核心数,可以通过
GOMAXPROCS
设置。 - G (Goroutine): 代表Go的并发执行单元。
2. 调度器的核心机制
2.1 工作窃取(Work Stealing)
当一个P的本地队列中没有可运行的Goroutine时,它会尝试从其他P的队列中“窃取”Goroutine来执行。这种机制可以平衡各个P的负载,避免某些P空闲而其他P过载。
2.2 抢占式调度
Go调度器是抢占式的,意味着它可以在Goroutine执行过程中强制切换执行权。Go 1.14引入了基于信号的抢占机制,确保长时间运行的Goroutine不会阻塞其他Goroutine的执行。
2.3 系统调用
当Goroutine执行系统调用时,调度器会将当前的M与P分离,并创建一个新的M来执行系统调用。这样可以避免系统调用阻塞整个P的执行。
3. 代码解析
3.1 调度器的初始化
调度器的初始化在runtime/proc.go
中的schedinit
函数中完成。该函数设置了P的数量、初始化全局队列等。
func schedinit() {
// 初始化P的数量
procs := int(ncpu)
if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
procs = n
}
if procresize(procs) != nil {
throw("unknown runnable goroutine during bootstrap")
}
}
3.2 Goroutine的创建与调度
Goroutine的创建通过go
关键字触发,最终会调用runtime.newproc
函数。该函数会将新的Goroutine放入当前P的本地队列中。
func newproc(siz int32, fn *funcval) {
argp := add(unsafe.Pointer(&fn), sys.PtrSize)
gp := getg()
pc := getcallerpc()
systemstack(func() {
newg := newproc1(fn, argp, siz, gp, pc)
_p_ := getg().m.p.ptr()
runqput(_p_, newg, true)
if mainStarted {
wakep()
}
})
}
3.3 调度循环
调度器的核心是调度循环,位于runtime/proc.go
中的schedule
函数。该函数会从当前P的本地队列、全局队列或其他P的队列中获取可运行的Goroutine并执行。
func schedule() {
_g_ := getg()
if _g_.m.locks != 0 {
throw("schedule: holding locks")
}
if _g_.m.lockedg != 0 {
stoplockedm()
execute(_g_.m.lockedg.ptr(), false) // Never returns.
}
// 调度循环
for {
if sched.gcwaiting != 0 {
gcstopm()
continue
}
if _g_.m.p.ptr().runqempty() {
// 如果本地队列为空,尝试从全局队列或其他P窃取Goroutine
gp, inheritTime = runqget(_g_.m.p.ptr())
if gp == nil {
gp, inheritTime = findrunnable() // 阻塞直到找到可运行的Goroutine
}
} else {
// 从本地队列获取Goroutine
gp, inheritTime = runqget(_g_.m.p.ptr())
}
execute(gp, inheritTime)
}
}
3.4 抢占机制
Go 1.14引入了基于信号的抢占机制,确保长时间运行的Goroutine不会阻塞其他Goroutine的执行。抢占机制的实现位于runtime/signal_unix.go
中。
func preemptM(mp *m) {
if atomic.Cas(&mp.signalPending, 0, 1) {
signalM(mp, sigPreempt)
}
}
4. 性能与并发
4.1 高效利用多核
通过M-P-G模型,Go调度器能够高效地利用多核CPU。每个P绑定到一个M上,M是操作系统线程,P负责调度Goroutine。P的数量通常等于CPU核心数,这样可以最大限度地利用CPU资源。
4.2 低延迟
Go调度器的抢占式调度和基于信号的抢占机制确保了低延迟。即使某个Goroutine长时间运行,调度器也能及时切换执行权,避免其他Goroutine被长时间阻塞。
4.3 高吞吐量
工作窃取机制确保了各个P之间的负载均衡,避免了某些P过载而其他P空闲的情况。这种机制提高了系统的整体吞吐量。
5. 总结
Golang的调度器通过M-P-G模型、工作窃取、抢占式调度等机制,实现了高效的并发和并行执行。调度器的设计使得Go语言在处理高并发场景时表现出色,能够充分利用多核CPU资源,同时保持低延迟和高吞吐量。
通过深入理解调度器的工作原理,开发者可以更好地编写高效的并发程序,充分利用Go语言的并发特性。