why goroutines executive order is the same between runs

Issue

I have a simple go program which has 2 consumer channels reading from one producer at the same time like this:

package main

import "fmt"

func main() {
    producer := make(chan int)
    wait := make(chan int)
    go func() {
        for i := 0; i < 1000; i++ {
            producer <- i
        }
        close(producer)
        wait <- 1
    }()

    go func() {
        count := 0
        for _ = range producer {
            count++
        }
        fmt.Printf("Consumer 1: %i\n", count)
    }()

    go func() {
        count := 0
        for _ = range producer {
            count++
        }
        fmt.Printf("Consumer 2: %i\n", count)
    }()
    <-wait
}

I was expecting two consumers get the same number of data or at least nearly equal. However the result is always:

Consumer 1: %!i(int=667)
Consumer 2: %!i(int=333)

It stays the same between multiple runs. Can anyone explain this behavior for me?

Environment:

go version go1.4.2 darwin/amd64
GOARCH="amd64"
GOBIN=""
GOCHAR="6"
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOOS="darwin"
GOPATH="/usr/local/go/:/Users/victor/Dropbox/projects/go/"
GORACE=""
GOROOT="/usr/local/Cellar/go/1.4.2/libexec"
GOTOOLDIR="/usr/local/Cellar/go/1.4.2/libexec/pkg/tool/darwin_amd64"
CC="clang"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fno-common"
CXX="clang++"
CGO_ENABLED="1"

Solution

The Go goroutine scheduling algorithm is not defined in The Go Programming Language Specification. It’s undefined and, therefore, it’s implementation and version dependent. Current implementations of Go use a cooperative scheduling scheme. The cooperative scheduling scheme relies on the goroutines to perform operations which yield to the scheduler from time to time. The scheduler code is in package runtime.

The program is dependent on Go channel operations.

The program is also dependent on the hardware (for example, number of CPUs), the operating system, and other running programs.

Your code should not expect a particular distribution from goroutine scheduling.

Go 1.4 defaults to GOMAXPROCS(1); for Go 1.5 and later, it defaults to NumCPU().

I’ve modified your program to fix bugs (additional wait statements), display diagnostic information, and yield to the scheduler (Gosched()) at certain points. Now, the program closely reproduces your results on Go 1.6 (devel tip), NumCPU() == 8, GOMAXPROCS(8) and on Go Playround, Go 1.5.1, NumCPU() == 1, GOMAXPROCS(1). Yielding to the goroutine scheduler at certain points in tight loops, and not at other points, was the key to reproducing your results.

package main

import (
    "fmt"
    "runtime"
)

func main() {
    fmt.Println(runtime.Version())
    fmt.Println(runtime.NumCPU())
    fmt.Println(runtime.GOMAXPROCS(0))
    producer := make(chan int, 100)
    wait := make(chan int, 100)
    go func() {
        for i := 0; i < 1000; i++ {
            producer <- i
            runtime.Gosched()
        }
        close(producer)
        wait <- 1
    }()
    go func() {
        count := 0
        for _ = range producer {
            count++
        }
        fmt.Printf("Consumer 1: %d\n", count)
        wait <- 1
    }()
    go func() {
        count := 0
        for _ = range producer {
            count++
            runtime.Gosched()
        }
        fmt.Printf("Consumer 2: %d\n", count)
        wait <- 1
    }()
    <-wait
    <-wait
    <-wait
}

With yielding (Gosched()):

> go run yield.go
8
8
Consumer 1: 668
Consumer 2: 332
> go run yield.go
devel +54b4b94 Sat Feb 6 23:33:23 2016 +0000
8
8
Consumer 2: 336
Consumer 1: 664
> go run yield.go
devel +54b4b94 Sat Feb 6 23:33:23 2016 +0000
8
8
Consumer 2: 333
Consumer 1: 667
>

playground: https://play.golang.org/p/griwLmsPDf

go1.5.1
1
1
Consumer 1: 674
Consumer 2: 326
go1.5.1
1
1
Consumer 1: 674
Consumer 2: 326
go1.5.1
1
1
Consumer 1: 674
Consumer 2: 326

For comparison, without yielding:

> go run noyield.go
devel +54b4b94 Sat Feb 6 23:33:23 2016 +0000
8
8
Consumer 1: 81
Consumer 2: 919
> go run noyield.go
devel +54b4b94 Sat Feb 6 23:33:23 2016 +0000
8
8
Consumer 1: 123
Consumer 2: 877
> go run noyield.go
devel +54b4b94 Sat Feb 6 23:33:23 2016 +0000
8
8
Consumer 1: 81
Consumer 2: 919
> go run noyield.go
devel +54b4b94 Sat Feb 6 23:33:23 2016 +0000
8
8
Consumer 2: 673
Consumer 1: 327

playground: https://play.golang.org/p/2KV1B04VUJ

go1.5.1
1
1
Consumer 1: 100
Consumer 2: 900
go1.5.1
1
1
Consumer 1: 100
Consumer 2: 900
go1.5.1
1
1
Consumer 1: 100
Consumer 2: 900

Answered By – peterSO

Answer Checked By – Senaida (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.