3

To settle some misunderstandings I have about goroutines, I went to the Go playground and ran this code:

package main

import (
    "fmt"
)

func other(done chan bool) {
    done <- true
    go func() {
        for {
            fmt.Println("Here")
        }
    }()
}

func main() {
    fmt.Println("Hello, playground")
    done := make(chan bool)
    go other(done)
    <-done
    fmt.Println("Finished.")
}

As I expected, Go playground came back with an error: Process took too long.

This seems to imply that the goroutine created within other runs forever.

But when I run the same code on my own machine, I get this output almost instantaneously:

Hello, playground.
Finished.

This seems to imply that the goroutine within other exits when the main goroutine finishes. Is this true? Or does the main goroutine finish, while the other goroutine continues to run in the background?

icza
  • 389,944
  • 63
  • 907
  • 827
  • Seems it also has a memory limit, when use too much memory, will terminate without finish. – Eric Apr 21 '23 at 19:22

2 Answers2

4

Edit: Default GOMAXPROCS has changed on the Go Playground, it now defaults to 8. In the "old" days it defaulted to 1. To get the behavior described in the question, set it to 1 explicitly with runtime.GOMAXPROCS(1).

Explanation of what you see:

On the Go Playground, GOMAXPROCS is 1 (proof).

This means one goroutine is executed at a time, and if that goroutine does not block, the scheduler is not forced to switch to other goroutines.

Your code (like every Go app) starts with a goroutine executing the main() function (the main goroutine). It starts another goroutine that executes the other() function, then it receives from the done channel - which blocks. So the scheduler must switch to the other goroutine (executing other() function).

In your other() function when you send a value on the done channel, that makes both the current (other()) and the main goroutine runnable. The scheduler chooses to continue to run other(), and since GOMAXPROCS=1, main() is not continued. Now other() launches another goroutine executing an endless loop. The scheduler chooses to execute this goroutine which takes forever to get to a blocked state, so main() is not continued.

And then the timeout of the Go Playground's sandbox comes as an absolution:

process took too long

Note that the Go Memory Model only guarantees that certain events happen before other events, you have no guarantee how 2 concurrent goroutines are executed. Which makes the output non-deterministic.

You are not to question any execution order that does not violate the Go Memory Model. If you want the execution to reach certain points in your code (to execute certain statements), you need explicit synchronization (you need to synchronize your goroutines).

Also note that the output on the Go Playground is cached, so if you run the app again, it won't be run again, but instead the cached output will be presented immediately. If you change anything in the code (e.g. insert a space or a comment) and then you run it again, it then will be compiled and run again. You will notice it by the increased response time. Using the current version (Go 1.6) you will see the same output every time though.

Running locally (on your machine):

When you run it locally, most likely GOMAXPROCS will be greater than 1 as it defaults to the number of CPU cores available (since Go 1.5). So it doesn't matter if you have a goroutine executing an endless loop, another goroutine will be executed simultaneously, which will be the main(), and when main() returns, your program terminates; it does not wait for other non-main goroutines to complete (see Spec: Program execution).

Also note that even if you set GOMAXPROCS to 1, your app will most likely exit in a "short" time as the scheduler imlementation will switch to other goroutines and not just execute the endless loop forever (however, as stated above, this is non-deterministic). And when it does, it will be the main() goroutine, and so when main() finishes and returns, your app terminates.

Playing with your app on the Go Playground:

As mentioned, by default GOMAXPROCS is 1 on the Go Playground. However it is allowed to set it to a higher value, e.g.:

runtime.GOMAXPROCS(2)

Without explicit synchronization, execution still remains non-deterministic, however you will observe a different execution order and a termination without running into a timeout:

Hello, playground
Here
Here
Here
...
<Here is printed 996 times, then:>
Finished.

Try this variant on the Go Playground.

icza
  • 389,944
  • 63
  • 907
  • 827
  • Looks like since 2020 Go Playground changed the default value of GOMAXPROCS to 8 instead of 1 (if you run proof link from the answer to see the new behavior). – Ruslan Isay May 18 '22 at 02:17
  • @RuslanIsay Thanks for the note. I added a note to the top of the answer. – icza May 19 '22 at 09:10
0

What you will see on screen is nondeterministic. Or more precisely if by any chance the true value you pass to channel is delayed you would see some "Here".

But usually the Stdout is buffered, it means it's not printed instantaneously but the data gets accumulated and after it gets to maximum buffer size it's printed. In your case before the "here" is printed the main function is already finished thus the process finishes.

The rule of thumb is: main function must be alive otherwise all other goroutines gets killed.

Paweł Szczur
  • 5,484
  • 3
  • 29
  • 32