讲师中心 微信公众号

工具推荐

视频效率加速
Seko
Seko

AI短剧一键生成成片

立刻MV
立刻MV

低成本歌曲 MV、视频生成

Laper
Laper

剧本策划到成片协作

updream

一键生成高质量推广成片

深度解析:Go语言Goroutine在多核环境下的创建开销与性能差异

千辰小哥_3277

千辰小哥_3277

发布时间:2025-10-07 12:43:00

|

176人浏览过

|

来源于php中文网

原创

深度解析:Go语言Goroutine在多核环境下的创建开销与性能差异

本文探讨了Go语言在多核环境下创建大量空闲Goroutine时,性能反而可能低于单核环境的现象。核心原因在于多核调度引入了更复杂的Go调度器开销和潜在的操作系统级上下文切换,而单核模式下,当主Goroutine不发生阻塞或主动让出CPU时,空闲Goroutine甚至可能从未真正执行,仅涉及高效的内部记账,从而显得更快。

在go语言的并发编程中,goroutine是轻量级的执行单元,其创建和调度通常被认为是高效的。然而,在某些特定场景下,尤其是在多核处理器环境中创建大量“空闲”goroutine时,我们可能会观察到一个反直觉的现象:其性能反而不如在单核模式下运行。这并非go语言或多核处理器的固有缺陷,而是go调度器在不同运行模式下处理goroutine生命周期的机制差异所致。

Go调度器与GOMAXPROCS

Go语言通过其用户态调度器(GPM模型)来管理Goroutine的执行。其中:

  • G (Goroutine):Go程序中的并发执行单元。
  • M (Machine):一个操作系统线程,负责执行Go代码。
  • P (Processor):一个逻辑处理器,代表一个M可以执行Go代码的上下文。P的数量由runtime.GOMAXPROCS()控制,默认值是CPU核心数。

runtime.GOMAXPROCS(n)函数设置了同时执行Go代码的操作系统线程(M)的最大数量,也即P的数量。当n大于1时,Go调度器会尝试将Goroutine分布到多个P上,从而利用多核并行执行。

单核环境下的Goroutine创建效率 (GOMAXPROCS(1))

当我们将GOMAXPROCS设置为1时,Go调度器只有一个P可用。这意味着所有的Goroutine都将由一个操作系统线程(M)来执行。在本文讨论的特定场景中,例如以下示例代码:

package main

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

func waitAround(die chan bool) {
    <-die // Goroutine在此等待,不执行任何计算或I/O
}

func main() {
    var startMemory runtime.MemStats
    runtime.ReadMemStats(&startMemory)

    start := time.Now()
    // cpus := runtime.NumCPU()
    runtime.GOMAXPROCS(1) // 强制单核运行
    die := make(chan bool)
    count := 100000
    for i := 0; i < count; i++ {
        go waitAround(die)
    }
    elapsed := time.Since(start)

    var endMemory runtime.MemStats
    runtime.ReadMemStats(&endMemory)

    fmt.Printf("Started %d goroutines\n%d CPUs\n%f seconds\n",
        count, 1, elapsed.Seconds())
    fmt.Printf("Memory before %d\nmemory after %d\n", startMemory.Alloc,
        endMemory.Alloc)
    fmt.Printf("%d goroutines running\n", runtime.NumGoroutine())
    fmt.Printf("%d bytes per goroutine\n", (endMemory.Alloc-startMemory.Alloc)/uint64(runtime.NumGoroutine()))

    close(die)
}

在上述代码中,主Goroutine连续创建了100,000个Goroutine,每个Goroutine都立即进入

立即学习“go语言免费学习笔记(深入)”;

在这种情况下,新创建的Goroutine虽然被分配了内存和调度器结构,但它们从未真正被调度到M上执行。对于Go调度器而言,这仅仅是内部数据结构的创建和维护(即“内部记账”)。由于没有实际的执行、上下文切换(无论是Go层面的还是OS层面的),整个过程非常高效和迅速。一旦close(die)被调用,这些等待的Goroutine会立即退出,它们的资源也很快会被垃圾回收。

Linuxdo Application
Linuxdo Application

为 Linux.do 注册撰写高通过率、去 AI 化的中文小作文。进行自适应调查以了解申请人的真实背景,...

下载

多核环境下的额外开销 (GOMAXPROCS(N > 1))

当GOMAXPROCS设置为大于1的值(例如runtime.NumCPU())时,Go调度器会尝试将Goroutine分布到多个P上,每个P绑定一个操作系统线程M。这种模式下,创建大量Goroutine会引入额外的开销:

  1. 调度器复杂性增加: Go调度器需要管理多个P,决定哪个Goroutine分配给哪个P,以及何时在P之间迁移Goroutine。这本身就比单P模式涉及更多的逻辑判断和同步开销。
  2. 操作系统级上下文切换: 当有多个P时,Go调度器会启动多个操作系统线程M。操作系统会根据其自身的调度策略在这些M之间进行上下文切换。操作系统级的上下文切换比Go语言用户态的Goroutine切换要重量级得多,因为它涉及到保存和恢复CPU寄存器、内存映射等,开销显著增加。
  3. Goroutine实际执行的可能性: 在多P环境下,即使Goroutine只是等待一个通道,它们也有更大的机会被Go调度器调度到某个M上并开始执行。即使只是执行到
  4. 缓存一致性开销: 多个M在不同CPU核心上运行时,可能涉及到共享内存数据的缓存同步问题,虽然对于本例中的空闲Goroutine影响较小,但也是多核环境下的潜在开销来源。

因此,在多核环境下,创建相同的100,000个空闲Goroutine,由于涉及更复杂的Go调度逻辑、潜在的操作系统级上下文切换以及Goroutine实际执行的开销,整体时间会显著增加。

示例代码分析

让我们再次审视提供的Go代码:

package main

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

func waitAround(die chan bool) {
    <- die // Goroutine在此等待
}

func main() {
    var startMemory runtime.MemStats
    runtime.ReadMemStats(&startMemory)

    start := time.Now()
    cpus := runtime.NumCPU()
    runtime.GOMAXPROCS(cpus) // 设置为多核运行
    die := make(chan bool)
    count := 100000
    for i := 0; i < count; i++ {
        go waitAround(die) // 创建大量Goroutine
    }
    elapsed := time.Since(start)

    var endMemory runtime.MemStats
    runtime.ReadMemStats(&endMemory)

    fmt.Printf("Started %d goroutines\n%d CPUs\n%f seconds\n",
        count, cpus, elapsed.Seconds())
    fmt.Printf("Memory before %d\nmemory after %d\n", startMemory.Alloc,
        endMemory.Alloc)
    fmt.Printf("%d goroutines running\n", runtime.NumGoroutine())
    fmt.Printf("%d bytes per goroutine\n", (endMemory.Alloc-startMemory.Alloc)/uint64(runtime.NumGoroutine()))

    close(die)
}

这段代码通过runtime.GOMAXPROCS(cpus)将Go调度器配置为使用所有可用的CPU核心。waitAround函数中的

性能差异的深层原因

根本原因在于Go调度器在不同GOMAXPROCS设置下的行为模式以及操作系统线程调度介入的程度。

  • 单核模式(GOMAXPROCS(1)):在主Goroutine不主动让出CPU的情况下,新创建的空闲Goroutine实际上只是在Go调度器的内部队列中注册,并分配了必要的栈空间和数据结构。它们并未真正获得CPU执行权,因此避免了任何实际的调度开销和操作系统上下文切换。这是一种非常高效的“记账”过程。
  • 多核模式(GOMAXPROCS(N > 1)):Go调度器会努力将Goroutine分布到多个M上。这种分布必然会增加调度器自身的复杂性,并引入操作系统在不同M之间进行上下文切换的开销。即使Goroutine只是执行到阻塞点,这个过程也比单核模式下“从未执行”要消耗更多资源。

注意事项与实践建议

  1. 场景特殊性: 这种“多核慢于单核”的现象是针对大量创建空闲Goroutine,且主Goroutine不发生阻塞或主动让出CPU的特定场景。对于执行实际计算或I/O操作的Goroutine,多核并行处理通常会带来显著的性能提升。
  2. Go调度器演进: Go语言的调度器一直在演进,例如Go 1.14引入了异步抢占,这使得长时间运行的Goroutine更容易被抢占。但这并不会根本改变上述核心机制:在主Goroutine不让出CPU的极端情况下,单核仍可能因为“不调度”而显得更快。
  3. 性能分析工具: 当遇到性能问题时,应使用pprof、strace(Linux下)等工具进行深入分析。例如,strace可以显示程序进行的系统调用,通过对比GOMAXPROCS(1)和GOMAXPROCS(cpus)下的strace输出,可以直观地看到多核模式下可能涉及更多的系统调用(如与线程调度相关的)。
  4. 避免过度优化: 通常情况下,我们不应手动设置GOMAXPROCS,而是让其保持默认值(CPU核心数),以充分利用多核处理器的并行能力。只有在经过详细性能分析后,确认是调度器配置导致的问题,才考虑调整。

总结

Go语言在多核环境下创建大量空闲Goroutine时,性能可能不如单核环境,这并非Go调度器效率低下,而是其在多核模式下为实现并发执行所付出的必要开销。单核模式下,在主Goroutine不让出CPU的特定情境中,新创建的空闲Goroutine可能从未真正执行,仅涉及高效的内部记账。而多核模式则引入了更复杂的Go调度器管理、潜在的操作系统级上下文切换以及Goroutine实际执行的开销。理解这些底层机制有助于我们更合理地设计和优化Go并发程序,避免在不适用的场景下对性能产生误判。

相关文章

数码产品性能查询
数码产品性能查询

该软件包括了市面上所有手机CPU,手机跑分情况,电脑CPU,电脑产品信息等等,方便需要大家查阅数码产品最新情况,了解产品特性,能够进行对比选择最具性价比的商品。

下载

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热门AI工具

更多
立刻MV
立刻MV Hot

立刻MV是一款AI文本写作工具,AI 音乐视频(MV)创作工具。

豆包大模型

豆包大模型是一款由字节跳动推出的企业级大语言模型服务平台。

DeepSeek

DeepSeek是一款面向对话、写作、编程和推理场景的AI大模型工具。

Loomy
Loomy Hot

一款AI工具,主要用于科大讯飞发布的桌面级 AI 助理,比 OpenClaw 更易用、更安全!,适合需要提升相关任务效率的用户。

Atoms
Atoms Hot

Atoms是一款AI智能体工具,第一支自动构建真实业务的 AI 团队。

函子Zion

一款支持AI搭建、可视化编辑和多端发布的全栈无代码开发平台。

WorkBuddy

一款AI办公效率工具,主要用于腾讯云推出的AI原生桌面智能体工作台,适合需要提升相关任务效率的用户。

二狗PPT
二狗PPT Hot

一款AI演示文稿工具,主要用于专为中式职场打造的AI PPT生成工具,适合需要提升相关任务效率的用户。

讯飞智文

一款面向学习和办公场景的AI文档创作工具,可辅助生成PPT与Word文档,提高资料整理和内容制作效率。

相关专题

更多
treenode的用法
treenode的用法

​在计算机编程领域,TreeNode是一种常见的数据结构,通常用于构建树形结构。在不同的编程语言中,TreeNode可能有不同的实现方式和用法,通常用于表示树的节点信息。更多关于treenode相关问题详情请看本专题下面的文章。php中文网欢迎大家前来学习。

2921

2023.12.01

C++ 高效算法与数据结构
C++ 高效算法与数据结构

本专题讲解 C++ 中常用算法与数据结构的实现与优化,涵盖排序算法(快速排序、归并排序)、查找算法、图算法、动态规划、贪心算法等,并结合实际案例分析如何选择最优算法来提高程序效率。通过深入理解数据结构(链表、树、堆、哈希表等),帮助开发者提升 在复杂应用中的算法设计与性能优化能力。

416

2025.12.22

深入理解算法:高效算法与数据结构专题
深入理解算法:高效算法与数据结构专题

本专题专注于算法与数据结构的核心概念,适合想深入理解并提升编程能力的开发者。专题内容包括常见数据结构的实现与应用,如数组、链表、栈、队列、哈希表、树、图等;以及高效的排序算法、搜索算法、动态规划等经典算法。通过详细的讲解与复杂度分析,帮助开发者不仅能熟练运用这些基础知识,还能在实际编程中优化性能,提高代码的执行效率。本专题适合准备面试的开发者,也适合希望提高算法思维的编程爱好者。

437

2026.01.06

C++ 数据结构与算法实现教程合集
C++ 数据结构与算法实现教程合集

以 C++ 为实现语言,系统讲解核心数据结构与算法,涵盖链表(单链表/双链表/环检测)、栈与队列(单调栈/优先队列)、二叉树(遍历/BST/AVL/红黑树)、哈希表(开地址法/链地址法)、图(邻接表/BFS/DFS/Dijkstra/拓扑排序)、常见排序算法(快排/归并/堆排/计数排序)的实现与复杂度分析,同时分享 LeetCode 刷题技巧、竞赛编程常用模板(二分/前缀和/滑动窗口/动态规划),帮助开发者夯实算法基础。

512

2026.05.09

堆和栈的区别
堆和栈的区别

堆和栈的区别:1、内存分配方式不同;2、大小不同;3、数据访问方式不同;4、数据的生命周期。本专题为大家提供堆和栈的区别的相关的文章、下载、课程内容,供大家免费下载体验。

7027

2023.07.18

堆和栈区别
堆和栈区别

堆(Heap)和栈(Stack)是计算机中两种常见的内存分配机制。它们在内存管理的方式、分配方式以及使用场景上有很大的区别。本文将详细介绍堆和栈的特点、区别以及各自的使用场景。php中文网给大家带来了相关的教程以及文章欢迎大家前来学习阅读。

2888

2023.08.10

线程和进程的区别
线程和进程的区别

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

4878

2023.08.10

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

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

3469

2023.09.06

Go-Zero API开发使用教程
Go-Zero API开发使用教程

本专题聚焦Go-Zero的API开发,拆解.api描述文件的语法结构、路由与分组定义、参数绑定与校验规则,说明统一响应体、JWT鉴权、跨域与接口文档生成的落地方式,并给出网关聚合与接口测试的实践建议。

40

2026.11.12

热门下载

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

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Python uv官方安装与入门文档
Python uv官方安装与入门文档

共0课时 | 0人学习

Buffalo框架快速入门指南
Buffalo框架快速入门指南

共0课时 | 0人学习

Valgrind支持平台说明
Valgrind支持平台说明

共0课时 | 0人学习

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

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn