一尘不染

runtime.Gosched 到底做了什么?

go

Tour of Go 网站的 go 1.5 发布之前的版本中,有一段代码如下所示。

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

困扰我的是,当runtime.Gosched()被删除时,程序不再打印“world”。

hello
hello
hello
hello
hello

为什么呢?如何runtime.Gosched()影响执行?


阅读 173

收藏
2021-12-06

共1个答案

一尘不染

笔记:

从 Go 1.5 开始, GOMAXPROCS 被设置为硬件的核心数:golang.org/doc/go1.5#runtime,低于 1.5 之前的原始答案。


当你在没有指定 GOMAXPROCS 环境变量的情况下运行 Go 程序时,Go goroutines 被安排在单个 OS 线程中执行。然而,为了使程序看起来是多线程的(这就是 goroutines 的用途,不是吗?),Go 调度程序有时必须切换执行上下文,以便每个 goroutine 可以完成它的工作。

正如我所说,当没有指定 GOMAXPROCS 变量时,Go 运行时只允许使用一个线程,因此当 goroutine 执行一些常规工作时,例如计算甚至 IO(映射到普通 C 函数)时,不可能切换执行上下文)。只有在使用 Go 并发原语时才能切换上下文,例如,当您打开多个通道时,或者(这是您的情况)当您明确告诉调度程序切换上下文时 - 这就是runtime.Gosched目的。

因此,简而言之,当一个 goroutine 中的执行上下文到达Goschedcall 时,调度程序会被指示将执行切换到另一个 goroutine。在您的情况下,有两个 goroutine,main(代表程序的“主”线程)和附加的,您使用go say. 如果您删除Gosched调用,执行上下文将永远不会从第一个 goroutine 转移到第二个,因此您没有“世界”。当Gosched存在时,调度程序将每个循环迭代的执行从第一个 goroutine 转移到第二个 goroutine,反之亦然,所以你有 ‘hello’ 和 ‘world’ 交错。

仅供参考,这称为“协作多任务”:goroutine 必须明确地将控制权交给其他 goroutine。大多数当代操作系统使用的方法称为“抢占式多任务”:执行线程不关心控制传输;调度程序将执行上下文透明地切换给它们。协作方法经常用于实现“绿色线程”,即逻辑并发协程,它们不会 1:1 映射到 OS 线程——这就是 Go 运行时及其 goroutines 的实现方式。

更新

我已经提到了 GOMAXPROCS 环境变量,但没有解释它是什么。是时候解决这个问题了。

当此变量设置为正数时N,Go 运行时将能够创建最多N本地线程,所有绿色线程都将在这些线程上进行调度。Native thread 一种由操作系统(Windows 线程、pthreads 等)创建的线程。这意味着如果N大于 1,goroutines 可能会被安排在不同的本地线程中执行,因此并行运行(至少,取决于您的计算机能力:如果您的系统基于多核处理器,它很可能这些线程将真正并行;如果您的处理器具有单核,那么在操作系统线程中实现的抢占式多任务处理将创建并行执行的可见性)。

可以使用runtime.GOMAXPROCS()函数设置 GOMAXPROCS 变量而不是预先设置环境变量。在你的程序中使用这样的东西而不是当前的main

func main() {
    runtime.GOMAXPROCS(2)
    go say("world")
    say("hello")
}

在这种情况下,您可以观察到有趣的结果。您可能会得到不均匀地交错打印的“hello”和“world”行,例如

hello
hello
world
hello
world
world
...

如果 goroutine 被安排为分离 OS 线程,就会发生这种情况。这实际上是抢占式多任务处理(或多核系统中的并行处理)的工作原理:线程是并行的,它们的组合输出是不确定的。顺便说一句,你可以离开或删除Gosched调用,当 GOMAXPROCS 大于 1 时,它似乎没有影响。

以下是我通过runtime.GOMAXPROCS调用在程序的几次运行中得到的结果。

hyperplex /tmp % go run test.go
hello
hello
hello
world
hello
world
hello
world
hyperplex /tmp % go run test.go
hello
world
hello
world
hello
world
hello
world
hello
world
hyperplex /tmp % go run test.go
hello
hello
hello
hello
hello
hyperplex /tmp % go run test.go
hello
world
hello
world
hello
world
hello
world
hello
world

看,有时输出很漂亮,有时则不然。行动中的不确定性:)

另一个更新

看起来在较新版本的 Go 编译器中,Go 运行时强制 goroutine 不仅在并发原语的使用上让步,而且在操作系统系统调用上也让步。这意味着执行上下文也可以在 IO 函数调用上在 goroutine 之间切换。因此,在最近的 Go 编译器中,即使 GOMAXPROCS 未设置或设置为 1,也有可能观察到不确定的行为。

2021-12-06