【问题标题】:What exactly does runtime.Gosched do?runtime.Gosched 到底是做什么的?
【发布时间】:2012-10-17 23:03:14
【问题描述】:

在a version prior to the release of go 1.5 of the Tour of Go website 中,有一段代码如下所示。

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() 对执行有何影响?

【问题讨论】:

    标签: concurrency go


    【解决方案1】:

    注意:

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


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

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

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

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

    更新

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

    当此变量设置为正数 N 时,Go 运行时将能够创建多达 N 的本机线程,所有绿色线程都将在其上调度。本机线程是一种由操作系统创建的线程(Windows 线程、pthread 等)。这意味着如果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 被调度为单独的操作系统线程,就会发生这种情况。这实际上是抢占式多任务的工作原理(或多核系统中的并行处理):线程是并行的,它们的组合输出是不确定的。顺便说一句,你可以离开或删除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 不仅在并发原语使用上产生,而且在 OS 系统调用上也产生。这意味着执行上下文也可以在 IO 函数调用时在 goroutine 之间切换。因此,在最近的 Go 编译器中,即使 GOMAXPROCS 未设置或设置为 1,也可以观察到不确定的行为。

    【讨论】:

    • 干得好!但是我在 go 1.0.3 下没有遇到这个问题,奇怪。
    • 这是真的。我刚刚用 go 1.0.3 进行了检查,是的,这种行为没有出现:即使使用 GOMAXPROCS == 1,程序也像 GOMAXPROCS >= 2 一样工作。似乎在 1.0.3 中调度程序已经过调整。
    • 我认为事情已经改变 w.r.t go 1.4 编译器。 OPs 问题中的示例似乎是创建操作系统线程,而这(--> gobyexample.com/atomic-counters )似乎创建了协作调度。如果这是真的,请更新答案
    • 从 Go 1.5 开始,GOMAXPROCS 设置为硬件的核心数:golang.org/doc/go1.5#runtime
    • @paulkon,是否需要Gosched() 取决于您的程序,而不取决于GOMAXPROCS 的值。抢占式多任务处理优于协作式多任务处理的效率还取决于您的程序。如果您的程序是 I/O 密集型的,那么使用异步 I/O 的协作多任务处理可能会比基于同步线程的 I/O 更有效(即具有更高的吞吐量);如果您的程序受 CPU 限制(例如长时间计算),那么协作式多任务处理将不太有用。
    【解决方案2】:

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

    【讨论】:

    • 好的,所以runtime.Gosched() 产生了。那是什么意思?它将控制权交还给主函数?
    • 在这种特定情况下是的。通常,它会要求调度程序以有意未指定的选择顺序启动并运行任何一个“就绪”的 goroutine。
    猜你喜欢
    • 2015-08-06
    • 2013-09-02
    • 2014-01-02
    • 2013-10-10
    • 2017-05-08
    • 2022-01-20
    • 2017-06-15
    • 2011-05-20
    • 2010-11-13
    相关资源
    最近更新 更多