0

0

Go Goroutine调度机制解析:单核与多核环境下的并发行为

聖光之護

聖光之護

发布时间:2025-11-29 15:08:00

|

909人浏览过

|

来源于php中文网

原创

Go Goroutine调度机制解析:单核与多核环境下的并发行为

本文深入探讨go语言goroutine的调度机制,解释为何并发执行并非总是即时并行。在默认或gomaxprocs设置为1的环境下,go运行时调度器倾向于在一个cpu核心上交替执行goroutine,导致看似串行运行一段时间后才出现交错。文章将分析这种现象,并阐述如何通过调整gomaxprocs来利用多核cpu实现真正的并行计算,从而更好地理解和测试go的并发能力。

理解Go Goroutine与调度器

Go语言通过轻量级的Goroutine实现了高效的并发编程。Goroutine是Go运行时管理的执行单元,它比操作系统线程更轻量,启动成本低,且可以创建数百万个。开发者通常期望当启动多个Goroutine时,它们能够“并排”或随机交错执行。然而,实际观察到的行为可能并非如此,特别是在CPU密集型任务中,有时会看到一个Goroutine长时间独占执行,随后另一个Goroutine接管,直到接近结束时才出现频繁的交替。这种现象的根源在于Go运行时调度器的内部工作机制以及GOMAXPROCS环境变量的影响。

Go调度器负责将Goroutine映射到操作系统线程上,再由操作系统线程映射到CPU核心上。需要明确的是,并发(Concurrency)与并行(Parallelism)是两个不同的概念:

  • 并发:指能够处理多个任务的能力,这些任务可能在同一时间段内交替进行。
  • 并行:指多个任务在同一时刻真正地同时执行,这通常需要多个CPU核心。

Go的调度器设计目标是实现高效的并发,但真正的并行执行则取决于可用的CPU核心数量以及GOMAXPROCS的配置。

GOMAXPROCS:控制并行度的关键

GOMAXPROCS是一个环境变量,它告诉Go运行时最多可以使用多少个操作系统线程来执行Go代码。这些操作系统线程被称为“处理器”(Processor,简称P),每个P可以运行一个M(Machine,即操作系统线程),而M则负责执行G(Goroutine)。

Zeemo AI
Zeemo AI

一款专业的视频字幕制作和视频处理工具

下载
  • Go 1.5版本之前:GOMAXPROCS的默认值为1。这意味着Go运行时只会创建一个操作系统线程来执行所有Goroutine。在这种情况下,即使系统有多个CPU核心,Go程序也只能在一个核心上运行。调度器会在这个单一的操作系统线程上,通过时间片轮转等方式,快速切换不同的Goroutine,从而实现并发,但无法实现真正的并行。
  • Go 1.5版本及之后:GOMAXPROCS的默认值被设置为机器上的逻辑CPU数量(通过runtime.NumCPU()获取)。这意味着Go运行时会尝试利用所有可用的CPU核心,创建相应数量的操作系统线程来执行Goroutine,从而在多核处理器上实现真正的并行。

当GOMAXPROCS设置为1时,所有Goroutine都将在同一个操作系统线程上运行。对于CPU密集型任务(如示例中的斐波那契计算),调度器可能允许一个Goroutine运行较长时间,直到它主动让出CPU(例如遇到I/O操作),或者达到调度器的抢占点。如果任务是纯计算且没有I/O,调度器切换的频率可能不高,导致长时间的“独占”现象。只有当两个Goroutine的计算量都接近尾声,或者在某个特定的调度时机,它们才可能出现频繁的交替执行,给人以“并排”执行的错觉。

示例分析与改进

考虑以下简化后的Go程序,它启动了两个CPU密集型Goroutine:

package main

import (
    "fmt"
    "runtime"
    "time"
)

// fib 计算斐波那契数列,一个CPU密集型任务
func fib(n int) int {
    if n <= 1 {
        return n
    }
    return fib(n-1) + fib(n-2)
}

// f 函数模拟一个执行单元,打印其进度
func f(from string) {
    for i := 0; i < 40; i++ {
        // 每次计算一个斐波那契数
        result := fib(i)
        fmt.Printf("%s fib( %d ): %d\n", from, i, result)
        // 可以在这里添加 runtime.Gosched() 尝试手动让出CPU,
        // 但对于CPU密集型任务,效果有限且不推荐常规使用
        // runtime.Gosched()
    }
}

func main() {
    // 打印当前GOMAXPROCS的值
    fmt.Printf("GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0))

    go f("|||") // 启动第一个Goroutine
    go f("---") // 启动第二个Goroutine

    // 等待Goroutine完成,避免主Goroutine提前退出
    // 生产环境中通常使用 sync.WaitGroup
    time.Sleep(5 * time.Second)
    fmt.Println("done")
}

观察到的现象(在GOMAXPROCS=1环境下): 程序输出可能显示|||先连续运行一段时间,然后---接管并连续运行,直到最后阶段才出现|||和---交替输出。这是因为在单核模式下,调度器将两个Goroutine安排在同一个操作系统线程上执行。当一个Goroutine开始执行CPU密集型任务时,它会尽可能地利用CPU,直到调度器强制切换。由于fib函数是纯计算,没有I/O阻塞,调度器切换的频率可能不高,导致长时间的“独占”现象。

如何实现真正的并行(在多核CPU上): 为了在多核CPU上观察到更明显的并行执行,我们需要确保GOMAXPROCS的值大于1。

  1. 通过环境变量设置: 在运行Go程序之前,设置GOMAXPROCS环境变量。

    • Linux/macOS: export GOMAXPROCS=2 (或更多,取决于你的CPU核心数)
    • Windows: set GOMAXPROCS=2 然后运行:go run your_program.go
  2. 通过代码设置(不推荐常规使用): 虽然可以在代码中使用runtime.GOMAXPROCS(n)来设置,但通常不建议这样做,因为这会覆盖用户或系统设置的环境变量,可能导致不灵活。现代Go版本(1.5+)默认已充分利用CPU核心。

    // 在main函数开始处添加
    // runtime.GOMAXPROCS(runtime.NumCPU()) // 默认行为,通常无需手动设置
    // runtime.GOMAXPROCS(2) // 强制设置为2个CPU核心

当GOMAXPROCS设置为大于1的值(例如2)时,Go运行时会创建多个操作系统线程。调度器可以将f("|||")和f("---")这两个Goroutine分别调度到不同的操作系统线程上,这些线程可以由操作系统的调度器在不同的CPU核心上同时执行,从而实现真正的并行。此时,你将更有可能观察到|||和---的输出更加随机和频繁地交错出现,甚至在多核处理器上同时输出。

注意事项与最佳实践

  • 理解调度器而非依赖其行为:Go调度器是高度优化的,但其具体行为(如切换时机、Goroutine运行顺序)是不确定的。编写并发代码时,不应依赖于Goroutine的特定执行顺序或交错模式。
  • 避免过度设置GOMAXPROCS:将GOMAXPROCS设置为远超CPU核心数的值通常没有益处,反而可能因为上下文切换的开销而降低性能。现代Go版本已经智能地处理了这个问题,通常无需手动调整。
  • I/O密集型与CPU密集型:对于I/O密集型任务,即使GOMAXPROCS=1,Goroutine也能通过I/O阻塞自动让出CPU,从而实现良好的并发。对于CPU密集型任务,GOMAXPROCS > 1是实现并行的前提。
  • 使用sync.WaitGroup进行同步:在实际应用中

相关专题

更多
线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

462

2023.08.10

Go中Type关键字的用法
Go中Type关键字的用法

Go中Type关键字的用法有定义新的类型别名或者创建新的结构体类型。本专题为大家提供Go相关的文章、下载、课程内容,供大家免费下载体验。

233

2023.09.06

go怎么实现链表
go怎么实现链表

go通过定义一个节点结构体、定义一个链表结构体、定义一些方法来操作链表、实现一个方法来删除链表中的一个节点和实现一个方法来打印链表中的所有节点的方法实现链表。

441

2023.09.25

go语言编程软件有哪些
go语言编程软件有哪些

go语言编程软件有Go编译器、Go开发环境、Go包管理器、Go测试框架、Go文档生成器、Go代码质量工具和Go性能分析工具等。本专题为大家提供go语言相关的文章、下载、课程内容,供大家免费下载体验。

244

2023.10.13

0基础如何学go语言
0基础如何学go语言

0基础学习Go语言需要分阶段进行,从基础知识到实践项目,逐步深入。php中文网给大家带来了go语言相关的教程以及文章,欢迎大家前来学习。

690

2023.10.26

Go语言实现运算符重载有哪些方法
Go语言实现运算符重载有哪些方法

Go语言不支持运算符重载,但可以通过一些方法来模拟运算符重载的效果。使用函数重载来模拟运算符重载,可以为不同的类型定义不同的函数,以实现类似运算符重载的效果,通过函数重载,可以为不同的类型实现不同的操作。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

187

2024.02.23

Go语言中的运算符有哪些
Go语言中的运算符有哪些

Go语言中的运算符有:1、加法运算符;2、减法运算符;3、乘法运算符;4、除法运算符;5、取余运算符;6、比较运算符;7、位运算符;8、按位与运算符;9、按位或运算符;10、按位异或运算符等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

221

2024.02.23

go语言开发工具大全
go语言开发工具大全

本专题整合了go语言开发工具大全,想了解更多相关详细内容,请阅读下面的文章。

275

2025.06.11

苹果官网入口直接访问
苹果官网入口直接访问

苹果官网直接访问入口是https://www.apple.com/cn/,该页面具备0.8秒首屏渲染、HTTP/3与Brotli加速、WebP+AVIF双格式图片、免登录浏览全参数等特性。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

147

2025.12.24

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
PostgreSQL 教程
PostgreSQL 教程

共48课时 | 6万人学习

Git 教程
Git 教程

共21课时 | 2.2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号