
go 语言通过 goroutine 实现了轻量级的并发。goroutine 是一种比操作系统线程更小的执行单元,由 go 运行时(runtime)负责调度。go 调度器负责将这些 goroutine 映射到少量的操作系统线程上运行。在 go 1.5 之前的版本中,当未明确设置 gomaxprocs 环境变量时,go 运行时默认只使用一个操作系统线程来执行所有的 goroutine。这意味着,即使有多个 goroutine,它们也只能在一个单线程上进行“并发”执行,即通过快速切换上下文来模拟并行。
在这种单线程模型下,Go 调度器需要一种机制来决定何时从一个 Goroutine 切换到另一个。Go 语言早期采用的是一种“协作式多任务处理”模型,即 Goroutine 必须主动或在特定Go并发原语(如 channel 操作)处让出控制权,调度器才能进行上下文切换。
runtime.Gosched() 函数正是这种协作式多任务处理的关键。当一个 Goroutine 调用 runtime.Gosched() 时,它会显式地告诉 Go 调度器:“我暂时不需要 CPU 了,请将执行权交给其他可运行的 Goroutine。” 调度器接收到这个指令后,就会暂停当前 Goroutine 的执行,并选择另一个 Goroutine 来运行。
考虑以下示例代码:
package main
import (
"fmt"
"runtime"
)
func say(s string) {
for i := 0; i < 5; i++ {
// runtime.Gosched() // 注释掉这一行
fmt.Println(s)
}
}
func main() {
go say("world") // 启动一个 Goroutine
say("hello") // main Goroutine 执行
}在 Go 1.5 之前或 GOMAXPROCS=1 的环境下,如果 runtime.Gosched() 被注释掉,程序的输出将是:
hello hello hello hello hello
这是因为 main Goroutine 在执行 say("hello") 循环时,没有显式地让出 CPU 控制权,也没有遇到任何 Go 并发原语(如 channel 操作)或系统调用,因此调度器无法将执行权转移给 say("world") Goroutine。main Goroutine 会一直运行直到其 say 函数执行完毕,然后程序退出,而 say("world") 甚至可能没有机会开始执行。
如果取消注释 runtime.Gosched():
package main
import (
"fmt"
"runtime"
)
func say(s string) {
for i := 0; i < 5; i++ {
runtime.Gosched() // 显式让出控制权
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}此时,程序的输出将是交替的:
hello world hello world hello world hello world hello
每次 say 函数循环迭代时,runtime.Gosched() 调用都会指示调度器切换到另一个 Goroutine。这样,say("hello") 和 say("world") 就能交替执行,实现了协作式的并发效果。
GOMAXPROCS 是一个重要的环境变量或运行时函数参数,它决定了 Go 运行时可以使用的操作系统线程的最大数量。
GOMAXPROCS = 1(或未设置,在 Go 1.5 之前默认值为 1): 如上所述,所有 Goroutine 都调度在一个操作系统线程上。这种情况下,runtime.Gosched() 或 Go 并发原语是实现 Goroutine 间上下文切换的主要方式。这是一种典型的“协作式多任务处理”模式。
GOMAXPROCS > 1(在 Go 1.5 之后,默认值为 CPU 核心数): 当 GOMAXPROCS 设置为大于 1 的值时,Go 运行时可以创建并使用多个操作系统线程。在这种情况下,Goroutine 可以在不同的操作系统线程上并行执行(如果系统是多核处理器),或者由操作系统调度器进行抢占式多任务处理(如果系统是单核)。
当 GOMAXPROCS > 1 时,Go 调度器的行为会变得更加复杂和“抢占式”。操作系统线程之间的切换由操作系统负责,而 Go 调度器会在这些线程上分配 Goroutine。这意味着,即使没有 runtime.Gosched() 调用,Goroutine 之间也可能在操作系统层面被抢占。
我们可以通过 runtime.GOMAXPROCS() 函数在程序中设置 GOMAXPROCS:
package main
import (
"fmt"
"runtime"
)
func say(s string) {
for i := 0; i < 5; i++ {
// runtime.Gosched() // 在 GOMAXPROCS > 1 时,此行效果可能不明显
fmt.Println(s)
}
}
func main() {
runtime.GOMAXPROCS(2) // 设置 GOMAXPROCS 为 2
go say("world")
say("hello")
}当 GOMAXPROCS(2) 被设置后,程序的输出可能会变得不确定,因为两个 Goroutine 可能在不同的操作系统线程上并行执行,或者由操作系统进行抢占式调度。例如,你可能会看到如下几种输出:
hello hello world hello world world ... (不确定的交错)
或者
hello world hello world hello world hello world hello
甚至
hello hello hello hello hello
这种不确定性是抢占式多任务处理的典型特征。在这种情况下,runtime.Gosched() 的显式让出控制权的效果会减弱,因为它不再是唯一的上下文切换机制。
Go 1.5 是 Go 调度器发展的一个重要里程碑。从 Go 1.5 开始:
因此,在现代 Go 版本中,像最初示例那样,在没有 runtime.Gosched() 时 say("world") 无法执行的情况,通常不会发生。调度器会在适当的时机(例如,fmt.Println 内部可能涉及系统调用)进行 Goroutine 切换,从而使得输出依然是交错的,尽管其具体顺序仍然是不确定的。
尽管现代 Go 调度器已经非常智能,但在某些特定场景下,runtime.Gosched() 仍然有其用武之地:
runtime.Gosched() 是 Go 语言中一个让 Goroutine 显式让出 CPU 控制权的重要函数。它在 Go 语言早期以及 GOMAXPROCS=1 的单线程调度模型下,对于实现 Goroutine 间的协作式多任务处理至关重要。随着 Go 调度器在 Go 1.5 之后的发展,特别是 GOMAXPROCS 默认值的改变和抢占机制的增强,runtime.Gosched() 在多数情况下不再是确保 Goroutine 切换的唯一或主要方式。然而,它仍然是一个有用的工具,可以在特定场景下(如防止 Goroutine 饥饿、测试并发行为)被用来微调调度器的行为。理解 runtime.Gosched() 的作用及其与 GOMAXPROCS 和 Go 调度器演进的关系,对于编写高效、健壮的 Go 并发程序至关重要。
以上就是深入理解 Go 语言调度器与 runtime.Gosched() 的作用的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号