109

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 (
    "fmt"
    "runtime"
)

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

func main() {
    go say("world")
    say("hello")
}

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?

nfirvine
  • 1,479
  • 12
  • 23
Jason Yeo
  • 3,602
  • 3
  • 30
  • 38

2 Answers2

175

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("world")
    say("hello")
}

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 :)

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.

Inanc Gumus
  • 25,195
  • 9
  • 85
  • 101
Vladimir Matveev
  • 120,085
  • 34
  • 287
  • 296
  • Great Job ! But i did not meet this issue under go 1.0.3 , wierd. – WoooHaaaa Nov 07 '12 at 02:32
  • 1
    This is true. I just checked this with go 1.0.3, and yes, this behavior did not appear: even with GOMAXPROCS == 1 the program worked as if GOMAXPROCS >= 2. Seems that in 1.0.3 the scheduler has been tweaked. – Vladimir Matveev Nov 07 '12 at 09:11
  • I think things have changed w.r.t go 1.4 compiler. Example in OPs question seems to be creating OS threads while this (--> https://gobyexample.com/atomic-counters ) seems to create cooperative scheduling. Please update the answer if this is true – tez Mar 06 '15 at 06:12
  • @tez, I seriously doubt that goroutines are now equivalent to OS threads. As far as I know, the number of native threads allocated for the program are still controlled by `GOMAXPROCS`. – Vladimir Matveev Mar 06 '15 at 07:52
  • 11
    As of Go 1.5, GOMAXPROCS is set to the number of cores of the hardware: https://golang.org/doc/go1.5#runtime – thepanuto Sep 16 '15 at 22:38
  • In Go 1.5, is `runtime.GoSched()` not needed, even if `GOMAXPROCS` set to 0 or 1? Also, is `preemptive multitasking` at the OS level more efficient than `cooperative multitasking`? – paulkon Nov 16 '15 at 23:55
  • 1
    @paulkon, whether or not `Gosched()` is needed depends on your program, it doesn't depend on `GOMAXPROCS` value. Efficiency of preemptive multitasking over the cooperative one also depends on your program. If your program is I/O-bound, then cooperative multitasking with async I/O will probably be more efficient (i.e. have more throughput) than synchronous thread-based I/O; if your program is CPU-bound (e.g. long computations), then cooperative multitasking will be much less useful. – Vladimir Matveev Nov 17 '15 at 09:00
  • Does this mean If I want to have async I/O with high throughput, I have to `GoSched` go-routine interleaving, otherwise I have a blocking/sychronous I/O? Does it also means that Go treats I/O bound processes the same way it treats CPU bound ones? Please correct me if I'm wrong. – securecurve Feb 19 '16 at 15:29
  • thanks for the detailed and well explained answer. – Mateen Bagheri Jul 09 '22 at 06:59
10

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.

zzzz
  • 87,403
  • 16
  • 175
  • 139
  • 3
    okay, so `runtime.Gosched()` yields. What does that mean? It yields the control back to the main function? – Jason Yeo Oct 28 '12 at 11:31
  • 9
    In this specific case yes. Generally it asks the scheduler to kick in and run any one of the "ready" goroutines in an intentionally unspecified selection order. – zzzz Oct 28 '12 at 11:37