返回

Golang 协程调度模型:手撕源码,深入浅出

后端

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 协程性能的一些方法包括合理分配协程数量、避免使用全局变量、使用通道进行协程通信等。