Golang 协程调度模型:手撕源码,深入浅出
2023-11-25 04:37:37
Go 协程调度:从 M:N 到 GMP,探索并发编程的新高度
前言
在当今高度并发的世界中,选择正确的编程语言至关重要。Go 凭借其高并发、高性能的特性在各大领域大放异彩。而支撑其高并发特性的核心技术之一便是协程。本篇博文将深入剖析 Go 协程调度模型的演进,从最初的 M:N 模型到如今的 GMP 模型,带你领略 Go 团队对并发编程的智慧结晶。
M:N 模型
M:N 模型是 Go 协程调度模型的雏形。它采用简单的模型,每个线程负责调度和执行与之一一对应的协程。这种模型容易理解,但当协程数量过多时,随之增长的线程数量会造成系统资源浪费和调度效率降低。
GMP 模型
为了解决 M:N 模型的缺陷,Go 团队引入了 GMP(Goroutine,M,P)模型。在该模型中,系统将协程划分成多个 G(协程)组,每个 G 组包含一定数量的协程。每个 P(Processor)是一个轻量级的调度器,负责调度和执行其所属的 G 组中的协程。而 M(Machine)则是操作系统线程,负责调度和管理 P。
GMP 模型通过引入 P 的概念,将协程的调度与执行解耦,使得系统可以根据实际情况动态调整 P 的数量,从而达到资源利用率和调度效率的最佳平衡。
源码分析
为了更深入地理解 Golang 协程调度模型,我们不妨手撕一下 Go 语言的源码。在 runtime/proc.go
文件中,我们可以看到 P 的定义:
type P struct {
id int32
status pstatus
link uintptr
schedlink uintptr
syscallstack *sys.Uintptr
m *M
mcache *mcache
g *g
runq runq
deferpool stackpool
deferpoolbuf []*defer
racectx racectx
traceseq int64
workqueue chan func()
startSeq uint64
_ [sys.CacheLineSize - ptrSize*11 - 8]byte
}
从 P 的定义中,我们可以看出它包含了调度相关的信息,例如运行队列、延迟池、调度状态等。此外,P 还与 M(线程)和 G(协程)关联,这体现了 GMP 模型中 P 的调度和执行分离的特性。
在 runtime/sched.go
文件中,我们可以看到 GMP 模型的调度过程:
func schedule() {
gp := getg().m.p.ptr().schedlink
if gp != nil {
p := gp.ptr().p
if p != getg().m.p.ptr() && p.status == _Prunning && cas(&gp.ptr().schedlink, gp, nil) {
return
}
}
for {
p := findrunnable()
if p == nil {
if preempt {
// ...
}
lock(&sched.lock)
p = sched.pickrunnable()
unlock(&sched.lock)
if p == nil {
// ...
}
// ...
}
// ...
}
}
这段代码展示了 Go 协程调度器的核心逻辑。调度器首先从当前线程的 P 中获取一个可调度的 G,如果没有找到,则遍历所有 P 以查找可调度的 G。一旦找到可调度的 G,调度器就会将 G 移出其当前 P 的调度队列并将其放入当前线程的 P 的调度队列中。
总结
Go 语言的协程调度模型经过了从 M:N 模型到 GMP 模型的演进,在不断优化和完善的过程中,GMP 模型通过引入 P 的概念,将 Goroutine 的调度与执行解耦,使得系统可以根据实际情况动态调整 P 的数量,从而达到资源利用率和调度效率的最佳平衡。通过手撕源码,我们深入剖析了 GMP 模型的实现细节,进一步理解了 Go 语言协程调度模型的精妙之处。
常见问题解答
Q1:M、P、G 三者之间有什么区别?
A1: M 是操作系统线程,负责调度和管理 P。P 是轻量级的调度器,负责调度和执行 G。G 是协程。
Q2:GMP 模型如何提高调度效率?
A2: GMP 模型通过引入 P 的概念,将协程的调度与执行解耦,使得系统可以根据实际情况动态调整 P 的数量,从而优化资源利用率和调度效率。
Q3:Go 协程的调度策略是什么?
A3: Go 协程的调度策略是一种抢占式调度,即当一个协程长时间执行时,调度器可以抢占该协程并调度其他协程。
Q4:Go 协程与 Java 线程有什么区别?
A4: Go 协程与 Java 线程最大的区别在于轻量级,Go 协程的创建和销毁开销远低于 Java 线程。
Q5:如何优化 Go 协程的性能?
A5: 优化 Go 协程性能的一些方法包括合理分配协程数量、避免使用全局变量、使用通道进行协程通信等。