99

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()被删除时,程序不再打印“世界”。

hello
hello
hello
hello
hello

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

4

2 回答 2

163

笔记:

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


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

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

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

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

更新

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

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

可以使用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

See, sometimes output is pretty, sometimes not. Indeterminism in action :)

Another update

Looks like that in newer versions of Go compiler Go runtime forces goroutines to yield not only on concurrency primitives usage, but on OS system calls too. This means that execution context can be switched between goroutines also on IO functions calls. Consequently, in recent Go compilers it is possible to observe indeterministic behavior even when GOMAXPROCS is unset or set to 1.

于 2012-10-28T11:37:00.640 回答
10

协同调度是罪魁祸首。在没有让步的情况下,另一个(比如“世界”)goroutine 可能在 main 终止之前/期间合法地获得零执行机会,根据规范终止所有 gorutines - 即。整个过程。

于 2012-10-28T11:19:12.593 回答