runtime.Gosched到底做什么?


85

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()影响执行力?

Answers:


143

注意:

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


在不指定GOMAXPROCS环境变量的情况下运行Go程序时,Go goroutine被安排在单个OS线程中执行。但是,要使程序看起来像是多线程的(那是goroutine的目的,不是吗?),Go调度程序有时必须切换执行上下文,因此每个goroutine都可以完成其工作。

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

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

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

更新资料

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

当此变量设置为正数时N,Go运行时将最多可以创建N本机线程,并在其上调度所有绿色线程。本机线程是操作系统创建的一种线程(Windows线程,pthreads等)。这意味着,如果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

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

另一个更新

看起来像在更高版本的Go编译器中,Go运行时不仅迫使goroutine产生并发基元用法,而且产生OS系统调用。这意味着可以在IO函数调用上的goroutine之间切换执行上下文。因此,在最新的Go编译器中,即使未设置GOMAXPROCS或将其设置为1,也可能观察到不确定行为。


做得好 !但是我在go 1.0.3下并没有遇到这个问题。
WoooHaaaa

1
这是真的。我只是用go 1.0.3进行了检查,是的,没有出现这种现象:即使使用GOMAXPROCS == 1,该程序的工作方式也好像GOMAXPROCS> =2。似乎在1.0.3中已对调度程序进行了调整。
弗拉基米尔·马特维夫

我认为wrt go 1.4编译器已改变。OP中的示例似乎正在创建OS线程,而此(-> gobyexample.com/atomic-counters)似乎在创建协作调度。如果是这样,请更新答案
tez 2015年

8
:作为围棋1.5,GOMAXPROCS被设定为硬件的核心的数量golang.org/doc/go1.5#runtime
thepanuto

1
@paulkon,是否Gosched()需要取决于您的程序,而不取决于GOMAXPROCS值。抢先式多任务处理在协作式上的效率还取决于您的程序。如果您的程序是受I / O约束的,那么与异步I / O协作的多任务处理可能比基于同步线程的I / O更为有效(即,具有更高的吞吐量)。如果您的程序受CPU限制(例如,较长的计算),则协作多任务处理的用途将大大减​​少。
弗拉基米尔·马特维夫

8

合作计划是罪魁祸首。如果不屈服,则另一个(例如“世界”)goroutine可能合法地在主程序终止之前/之时获得零执行机会,按照规范,这将终止所有gorutines-即。整个过程。


1
好,这样就runtime.Gosched()可以了。那是什么意思?它将控制权交还给主要功能吗?
詹森·杨

5
在这种情况下,可以。通常,它要求调度程序以故意未指定的选择顺序启动并运行任何一个“就绪” goroutine。
zzzz 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.