0

I am absolute newbie to concurrency in Go. I was trying to produce race condition with two gouroutines and wrote the following code:

var x int = 2

func main() {

    go f1(&x)

    go f2(&x)

    time.Sleep(time.Second)
    fmt.Println("Final value of x:", x)

}

func f1(px *int) {
    for i := 0; i < 10; i++ {
        *px = *px * 2
        fmt.Println("f1:", *px)
    }
}

func f2(px *int) {
    for i := 0; i < 10; i++ {
        *px = *px + 1
        fmt.Println("f2:", *px)
    }
}

And in each variant of output there are all the f2's output lines in console and only after that there are f1's outputs. Here's an example:

f2: 3
f2: 4
f2: 5
f2: 6
f2: 7
f2: 8
f2: 9
f2: 10
f2: 21
f2: 22
f1: 20
f1: 44
f1: 88
f1: 176
f1: 352
f1: 704
f1: 1408
f1: 2816
f1: 5632
f1: 11264
Final value of x: 11264

But you can see that indeed some of f1's executions were made in between of f2's executions:

f2: 10
f2: 21

So that i have two questions:

  1. Why all the Printl() of f1 executes strictly after execution of f2's Println() (I thought they must be mixed somehow)
  2. Why when I change the order of goroutines in the code
    go f2(&x)
    go f1(&x)

instead of

    go f1(&x)
    go f2(&x)

the order of output lines changes vice versa, f1's first, f2'2 second. I mean how the order of gouroutines in code affects their execution?

Phelizer
  • 309
  • 2
  • 17
  • The order of execution of goroutines is non deterministic. Same for interruption of goroutines by other goroutines. It also depends on the number of cores. On a real workload, it won't matter. – Marc Aug 16 '20 at 14:23

1 Answers1

2

Firstly, the behavior you are seeing is due to a tight-loop. The Go scheduler cannot reasonable know how to share the workload, since your loops are short and don't take significant amounts of time (below the 10ms threshold for instance)

How the Go scheduler works is a very board topic and has changed across Go versions, but to quote this artcile:

If loops don’t contain any preemption points (like function calls, or allocate memory), they will prevent other goroutines from running

with preemption typically not occurring until 10ms later.

In the real world a processing loop will typically invoke some blocking call (DB operation, REST/gRPC call etc) - this will give a cue to the Go scheduler to set other goroutines as "Runnable". You can simulate this in your code by inserting a time.Sleep into your loops: https://play.golang.org/p/_C3QOUMNOaU

There are other methods to relinquish (runtime.Gosched) but these techniques should generally be avoided. Avoid tight-loops and let the schedule do its thing.

Execution Ordering

When multiple goroutines are involved - and as @Marc commented - without coordination between the goroutines, the order of execution is non-deterministic.

Go has many tools at it's disposal to coordinate go-routine activities:

that block the current goroutine and allow other goroutines to be scheduled. Using these techniques guarantee the precise ordering of larger tasks.

Predicting the execution order, however, of individual instructions that run between these coordination checkpoints is impossible.

colm.anseo
  • 19,337
  • 4
  • 43
  • 52