Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

"Select" statement inside of goroutine with for loop

Could somebody please explain, why if goroutine has endless for loop and select inside of the loop, a code in the loop is run only one time?

package main

import (
    "time"
)

func f1(quit chan bool){
    go func() {
        for {
            println("f1 is working...")
            time.Sleep(1 * time.Second)

            select{
            case <-quit:
            println("stopping f1")
            break
            }
        }   
    }()
}

func main() {
    quit := make(chan bool)
    f1(quit)
    time.Sleep(4 * time.Second)
}

Output:

f1 is working...
Program exited.

But if "select" is commented out:

package main

import (
    "time"
)

func f1(quit chan bool){
    go func() {
        for {
            println("f1 is working...")
            time.Sleep(1 * time.Second)

            //select{
            //case <-quit:
            //println("stopping f1")
            //break
            //}
        }   
    }()
}

func main() {
    quit := make(chan bool)
    f1(quit)
    time.Sleep(4 * time.Second)
}

Output:

f1 is working...
f1 is working...
f1 is working...
f1 is working...
f1 is working...

https://play.golang.org/p/MxKy2XqQlt8

like image 983
Dimaf Avatar asked Dec 04 '22 19:12

Dimaf


1 Answers

A select statement without a default case is blocking until a read or write in at least one of the case statements can be executed. Accordingly, your select will block until a read from the quit channel is possible (either of a value or the zero value if the channel is closed). The language spec provides a concrete description of this behavior, specifically:

If one or more of the communications [expressed in the case statements] can proceed, a single one that can proceed is chosen via a uniform pseudo-random selection. Otherwise, if there is a default case, that case is chosen. If there is no default case, the "select" statement blocks until at least one of the communications can proceed.

Other code issues

Caution! break applies to the select statement

However, even if you did close the quit channel to signal shutdown of your program, your implementation would likely not have the desired effect. A (unlabelled) call to break will terminate execution of the inner-most for, select or switch statement within the function. In this case, the select statement will break and the for loop will run through again. If quit was closed, it will run forever until something else stops the program, otherwise it will block again in select (Playground example)

Closing quit will (probably) not immediately stop the program

As noted in the comments, the call to time.Sleep blocks for a second on each iteration of the loop, so any attempt to stop the program by closing quit will be delayed for up to approximately a second before the goroutine checks quit and escapes. It is unlikely this sleep period must complete in its entirety before the program stops.

More idiomatic Go would block in the select statement on receiving from two channels:

  • The quit channel
  • The channel returned by time.After – this call is an abstraction around a Timer which sleeps for a period of time and then writes a value to the provided channel.

Resolution

Solution with minimal changes

A resolution to fix your immediate issue with minimal changes to your code would be:

  • Make the read from quit non-blocking by adding a default case to the select statement.
  • Ensuring the goroutine actually returns when the read from quit succeeds:
    • Label the for loop and use a labelled call to break; or
    • return from the f1 function when it is time to quit (preferred)

Depending on your circumstances, you may find it more idiomatic Go to use a context.Context to signal termination, and to use a sync.WaitGroup to wait for the goroutine to finish before returning from main.

package main

import (
    "fmt"
    "time"
)

func f1(quit chan bool) {
    go func() {
        for {
            println("f1 is working...")
            time.Sleep(1 * time.Second)

            select {
            case <-quit:
                fmt.Println("stopping")
                return
            default:
            }
        }
    }()
}

func main() {
    quit := make(chan bool)
    f1(quit)
    time.Sleep(4 * time.Second)
    close(quit)
    time.Sleep(4 * time.Second)
}

Working example

(Note: I have added an additional time.Sleep call in your main method to avoid this returning immediately after the call to close and terminating the program.)

Fixing the sleep blocking issue

To fix the additional issue regarding the blocking sleep preventing immediate quit, move the sleep to a timer in the select block. Modifying your for loop as per this playground example from the comments does exactly this:

for {
    println("f1 is working...")

    select {
    case <-quit:
        println("stopping f1")
        return
    case <-time.After(1 * time.Second):
        // repeats loop
    }
}

Related literature

  • Go-by-example: Non-blocking channel operations
  • Dave Cheney's Channel axioms, specifically the fourth axiom "A receive from a closed channel returns the zero value immediately."
like image 71
Cosmic Ossifrage Avatar answered Jan 19 '23 05:01

Cosmic Ossifrage