
本文深入探讨go语言goroutine的调度机制,解释为何并发执行并非总是即时并行。在默认或gomaxprocs设置为1的环境下,go运行时调度器倾向于在一个cpu核心上交替执行goroutine,导致看似串行运行一段时间后才出现交错。文章将分析这种现象,并阐述如何通过调整gomaxprocs来利用多核cpu实现真正的并行计算,从而更好地理解和测试go的并发能力。
Go语言通过轻量级的Goroutine实现了高效的并发编程。Goroutine是Go运行时管理的执行单元,它比操作系统线程更轻量,启动成本低,且可以创建数百万个。开发者通常期望当启动多个Goroutine时,它们能够“并排”或随机交错执行。然而,实际观察到的行为可能并非如此,特别是在CPU密集型任务中,有时会看到一个Goroutine长时间独占执行,随后另一个Goroutine接管,直到接近结束时才出现频繁的交替。这种现象的根源在于Go运行时调度器的内部工作机制以及GOMAXPROCS环境变量的影响。
Go调度器负责将Goroutine映射到操作系统线程上,再由操作系统线程映射到CPU核心上。需要明确的是,并发(Concurrency)与并行(Parallelism)是两个不同的概念:
Go的调度器设计目标是实现高效的并发,但真正的并行执行则取决于可用的CPU核心数量以及GOMAXPROCS的配置。
GOMAXPROCS是一个环境变量,它告诉Go运行时最多可以使用多少个操作系统线程来执行Go代码。这些操作系统线程被称为“处理器”(Processor,简称P),每个P可以运行一个M(Machine,即操作系统线程),而M则负责执行G(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。
通过环境变量设置: 在运行Go程序之前,设置GOMAXPROCS环境变量。
通过代码设置(不推荐常规使用): 虽然可以在代码中使用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调度机制解析:单核与多核环境下的并发行为的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号