What exactly does runtime.Gosched do?

huangapple go评论82阅读模式
英文:

What exactly does runtime.Gosched do?

问题

在go 1.5版本之前的Go之旅网站中,有一段代码看起来像这样。

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

英文:

In a version prior to the release of go 1.5 of the Tour of Go website, there's a piece of code that looks like this.

package main

import (
	&quot;fmt&quot;
	&quot;runtime&quot;
)

func say(s string) {
	for i := 0; i &lt; 5; i++ {
		runtime.Gosched()
		fmt.Println(s)
	}
}

func main() {
	go say(&quot;world&quot;)
	say(&quot;hello&quot;)
}

The output looks like this:

hello
world
hello
world
hello
world
hello
world
hello

What is bothering me is that when runtime.Gosched() is removed, the program no longer prints "world".

hello
hello
hello
hello
hello

Why is that so? How does runtime.Gosched() affect the execution?

答案1

得分: 174

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


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

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

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

顺便说一下,这被称为'协作式多任务处理':goroutine必须显式地将控制权让给其他goroutine。大多数现代操作系统中使用的方法被称为'抢占式多任务处理':执行线程不关心控制转移;调度器会透明地为它们切换执行上下文。协作式方法经常用于实现'绿色线程',即逻辑并发协程,它们不是1:1映射到操作系统线程 - 这就是Go运行时及其goroutine的实现方式。

更新

我提到了GOMAXPROCS环境变量,但没有解释它是什么。现在是时候解释一下了。

当将此变量设置为正数N时,Go运行时将能够创建多达N个本机线程,所有绿色线程将在这些线程上调度。本机线程是由操作系统创建的一种线程(Windows线程、pthreads等)。这意味着如果N大于1,goroutine可能会被调度到不同的本机线程中执行,因此可以并行运行(至少在你的计算机能力范围内:如果你的系统基于多核处理器,这些线程很可能是真正并行的;如果你的处理器只有一个核心,那么在操作系统线程中实现的抢占式多任务处理将创建出并行执行的可见性)。

可以使用runtime.GOMAXPROCS()函数来设置GOMAXPROCS变量,而不是预先设置环境变量。在你的程序中,使用以下代码替换当前的main函数:

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

在这种情况下,你可能会观察到有趣的结果。如果goroutine被调度到单独的操作系统线程中,你可能会看到'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

看,有时输出很好,有时不好。这就是不确定性的体现 What exactly does runtime.Gosched do?

另一个更新

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

英文:

Note:

As of Go 1.5, GOMAXPROCS is set to the number of cores of the hardware: golang.org/doc/go1.5#runtime, below the original answer before 1.5.


When you run Go program without specifying GOMAXPROCS environment variable, Go goroutines are scheduled for execution in single OS thread. However, to make program appear to be multithreaded (that's what goroutines are for, aren't they?), the Go scheduler must sometimes switch the execution context, so each goroutine could do its piece of work.

As I said, when GOMAXPROCS variable is not specified, Go runtime is only allowed to use one thread, so it is impossible to switch execution contexts while goroutine is performing some conventional work, like computations or even IO (which is mapped to plain C functions). The context can be switched only when Go concurrency primitives are used, e.g. when you switch on several chans, or (this is your case) when you explicitly tell the scheduler to switch the contexts - this is what runtime.Gosched is for.

So, in short, when execution context in one goroutine reaches Gosched call, the scheduler is instructed to switch the execution to another goroutine. In your case there are two goroutines, main (which represents 'main' thread of the program) and additional, the one you have created with go say. If you remove Gosched call, the execution context will never be transferred from the first goroutine to the second, hence no 'world' for you. When Gosched is present, the scheduler transfers the execution on each loop iteration from first goroutine to the second and vice versa, so you have 'hello' and 'world' interleaved.

FYI, this is called 'cooperative multitasking': goroutines must explicitly yield the control to other goroutines. The approach used in most contemporary OSes is called 'preemptive multitasking': execution threads are not concerned with control transferring; the scheduler switches execution contexts transparently to them instead. Cooperative approach is frequently used to implement 'green threads', that is, logical concurrent coroutines which do not map 1:1 to OS threads - this is how Go runtime and its goroutines are implemented.

Update

I've mentioned GOMAXPROCS environment variable but didn't explain what is it. It's time to fix this.

When this variable is set to a positive number N, Go runtime will be able to create up to N native threads, on which all green threads will be scheduled. Native thread a kind of thread which is created by the operating system (Windows threads, pthreads etc). This means that if N is greater than 1, it is possible that goroutines will be scheduled to execute in different native threads and, consequently, run in parallel (at least, up to your computer capabilities: if your system is based on multicore processor, it is likely that these threads will be truly parallel; if your processor has single core, then preemptive multitasking implemented in OS threads will create a visibility of parallel execution).

It is possible to set GOMAXPROCS variable using runtime.GOMAXPROCS() function instead of pre-setting the environment variable. Use something like this in your program instead of the current main:

func main() {
    runtime.GOMAXPROCS(2)
    go say(&quot;world&quot;)
    say(&quot;hello&quot;)
}

In this case you can observe interesting results. It is possible that you will get 'hello' and 'world' lines printed interleaved unevenly, e.g.

hello
hello
world
hello
world
world
...

This can happen if goroutines are scheduled to separate OS threads. This is in fact how preemptive multitasking works (or parallel processing in case of multicore systems): threads are parallel, and their combined output is indeterministic. BTW, you can leave or remove Gosched call, it seems to have no effect when GOMAXPROCS is bigger than 1.

The following is what I got on several runs of the program with runtime.GOMAXPROCS call.

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 What exactly does runtime.Gosched do?

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.

答案2

得分: 10

合作调度是罪魁祸首。如果不进行让步,其他(比如说“world”)的goroutine在主goroutine终止之前/之时可能合法地没有机会执行,根据规范,主goroutine终止会导致所有goroutine终止 - 即整个进程终止。

英文:

Cooperative scheduling is the culprit. Without yielding, the other (say "world") goroutine may legally get zero chances to execute before/when main terminates, which per specs terminates all gorutines - ie. the whole process.

huangapple
  • 本文由 发表于 2012年10月28日 18:12:51
  • 转载请务必保留本文链接:https://go.coder-hub.com/13107958.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定