浅析Golang中调度器的关键机制与性能

 更新时间:2025年03月05日 10:09:31   作者:Ai 编码  
Golang的调度器是其并发模型的核心组件,负责管理Goroutine的调度和执行,本文将从理论和代码层面分析Golang调度器的关键机制,感兴趣的可以了解下

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

到此这篇关于浅析Golang中调度器的关键机制与性能的文章就介绍到这了,更多相关Golang调度器内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 详解如何修改Go结构体的私有字段

    详解如何修改Go结构体的私有字段

    在 Go 语言中,结构体字段的访问权限是由字段名的首字母决定的:首字母大写表示公共字段(public),首字母小写表示私有字段(private),本文给大家介绍了如何修改Go结构体的私有字段,需要的朋友可以参考下
    2025-01-01
  • Go语言中常量的实现

    Go语言中常量的实现

    Go语言支持单常量和多常量的定义方法,通过const关键字实现,常量用于存储不变的值,如圆周率或固定的错误信息,旨在提高代码的维护性和可读性,感兴趣的可以了解一下
    2024-10-10
  • Golang操作ES进行交互的实现实例

    Golang操作ES进行交互的实现实例

    本文详细介绍了如何使用Golang与Elasticsearch进行交互,包括创建项目、安装包、连接ES、操作索引、插入、查询和删除文档,具有一定的参考价值,感兴趣的可以了解一下
    2025-11-11
  • Golang请求fasthttp实践

    Golang请求fasthttp实践

    本文主要介绍了Golang请求fasthttp实践,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-11-11
  • Golang常用的几种密码加密方式分享

    Golang常用的几种密码加密方式分享

    这篇文章给大家介绍了Golang常用的几种密码加密方式,加密有两种方式,一种是直接加密,一种是盐值加密,直接加密指的是将原始密码直接进行加密,盐值加密则是在进行密码加密之前,文中有详细的代码示例,需要的朋友可以参考下
    2023-08-08
  • golang struct扩展函数参数命名警告解决方法

    golang struct扩展函数参数命名警告解决方法

    今天在使用VSCode编写golang代码时,定义一个struct,扩展几个方法,需要的朋友可以参考下
    2017-02-02
  • golang如何修改json文件内容的方法示例

    golang如何修改json文件内容的方法示例

    这篇文章主要介绍了golang如何修改json文件内容的方法示例,使用一个例子说明golang如何访问和修改json文件,有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-10-10
  • golang1.21泛型函数全面讲解

    golang1.21泛型函数全面讲解

    在Go编程语言中,泛型一直是一个备受期待的特性,随着Go 1.21的发布,本文旨在提供Go 1.21中泛型的详细探索,阐明它们的优点、语法、实现和最佳实践,希望对大家有所帮助
    2023-09-09
  • golang结合mysql设置最大连接数和最大空闲连接数

    golang结合mysql设置最大连接数和最大空闲连接数

    本文介绍golang 中连接MySQL时,如何设置最大连接数和最大空闲连接数,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-02-02
  • 浅谈goland导入自定义包时出错(一招解决问题)

    浅谈goland导入自定义包时出错(一招解决问题)

    这篇文章主要介绍了浅谈goland导入自定义包时出错(一招解决问题),具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12

最新评论