Bootstrap

Golang调度器是性能与并发的关键因素

文章精选推荐

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语言的并发特性。

;