浅析Golang中调度器的关键机制与性能
目录
- 1. 调度器的基本概念
- 1.1 Goroutine
- 1.2 M-P-G 模型
- 2. 调度器的核心机制
- 2.1 工作窃取(Work Stealing)
- 2.2 抢占式调度
- 2.3 系统调用
- 3. 代码解析
- 3.1 调度器的初始化
- 3.2 Goroutine的创建与调度
- 3.3 调度循环
- 3.4 抢占机制
- 4. 性能与并发
- 4.1 高效利用多核
- 4.2 低延迟
- 4.3 高吞吐量
- 5. 总结
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(unsaandroidfe.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 { // 从本地队列www.devze.com获取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长时间运行,调度器也能及时切换js执行权,避免其他Goroutine被长时间阻塞。
4.3 高吞吐量
工作窃取机制确保了各个P之间的负载均衡,避免了某些P过载而其他P空闲的情况。这种机制提高了系统的整体吞吐量。
5. 总结
Golang的调度器通过M-P-G模型、工作窃取、抢占式调度等机制,实现了高效的并发和并行执行。调度器的设计使得Go语言在处理高并发场景时表现出色,能够充分利用多核CPU资源,同时保持低延迟和高吞吐量。
通过深入理解调度器的工作原理,开发者可以更好地编写高效的并发程序,充分利用Go语言的并发特性。
到此这篇关于浅析Golang中调度器的关键机制与性能的文章就介绍到这了,更多相关Golang调度器内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!
精彩评论