2

I've got a goroutine that's playing some audio infinitely play(). In order to keep the play() alive, I've got the calling function running an infinite for loop afterward.

The unexpected thing is that a barebones loop does not seem to let the function play infinitely and I'm at a loss as to why. However, if I add a simple time.Sleep(time.Second) into the body of the for-loop, it runs seemingly infinitely. Any idea as to why?

To visualize:

func PlaysForAFewSeconds() {
    go play()
    for {
    }
}

^plays for a few seconds but never breaks out

func PlaysForever() {
    go play()
    for {
        time.Sleep(time.Second)
    }
}

^ plays forever.

I'm guessing that this has something to do with how play() is implemented but I'm hoping this is a common enough problem that someone recognizes this symptom. Thanks.

Lucian Thorr
  • 1,997
  • 1
  • 21
  • 29
  • 2
    "In order to keep the play() alive, I've got the calling function running an infinite for loop afterward." --- this is plain wrong. After you started a goroutine it has no connection to the code that called it. Having any sort of loop makes little to no sense there. If `play()` is blocking just run it without goroutines. – zerkms Apr 25 '19 at 23:25
  • I agree that it's bad code but it doesn't provide any insight as to why/how a goroutine would behave differently depending on the body of the for-loop. – Lucian Thorr Apr 25 '19 at 23:30
  • How many cpu's are available to the process? What go version do you use? Do you set `runtime.GOMAXPROCS` manually anywhere? – zerkms Apr 25 '19 at 23:32
  • 4 cpus, not messing with runtime.GOMAXPROCS. – Lucian Thorr Apr 25 '19 at 23:38
  • That really must be it. Even putting an `fmt.Println()` inside the loop allows it to play continuously. It just had me scratching my head as to why that wouldn't work. Thanks. @zerkms And don't worry, I won't really use the code above:) – Lucian Thorr Apr 25 '19 at 23:41
  • @LucianThorr `fmt.Println` (unless you change the default writer) incurs a syscall, which is a safe-point, so it probably has something to do with a scheduler not being able to work properly under those circumstances. – zerkms Apr 25 '19 at 23:44
  • 6
    A busy loop is always a programming error. Even if the scheduler could interrupt it, It’s not useful and wastes cpu. Just don’t do that. – JimB Apr 26 '19 at 00:01
  • @JimB but if OP has 4 CPUs available for the process does it matter? Why wouldn't it still work even with a busy loop? – zerkms Apr 26 '19 at 02:13
  • as an aside, we can agree `for {}` is bad. If you want to ever block a go-routine indefinitely and not create a CPU-wasting tight-loop, use `select {}` instead. – colm.anseo Apr 26 '19 at 13:56
  • See https://stackoverflow.com/questions/33524477/gomaxprocs-already-be-2-but-program-still-hang/33524863#33524863. In the current implementation it prevents the scheduler from allowing the gc to run a brief stop the world phase. Your user goroutines are not the only ones that need to run. In general, goroutines are not threads and are cooperatively scheduled. – JimB Apr 26 '19 at 15:26
  • This answer is helpful https://stackoverflow.com/a/35228972/2704032 – Vishrant Jul 25 '22 at 15:57

1 Answers1

2

The assembly that for { } generates is jmp self, where self is the location of the jmp instruction. In other words, the CPU will just keep running jmp instructions as fast as it can. The CPU can run n instructions per second, and it doesn't matter if this is a useless jmp or an actually useful instruction.

This is known as a "busy wait" or "spin lock", and this behaviour is not specific to Go. Most (all?) programming languages behave like this.

There are some uses for such loops, but in Go they can often be replaced with channels:

// Simulate a function that takes 1s to complete.
func play(ch chan struct{}) {
    fmt.Println("play")
    time.Sleep(1 * time.Second)
    ch <- struct{}{}
}

func PlaysForAFewSeconds() {
    wait := make(chan struct{})
    go play(wait)
    <-wait
}

func PlaysForever() {
    wait := make(chan struct{})
    for {
        go play(wait)
        <-wait
    }
}

Reading from a channel (<-wait) is blocking and doesn't use any CPU. I used an empty anonymous struct, which looks a bit ugly, as that doesn't allocate any memory.

Martin Tournoij
  • 26,737
  • 24
  • 105
  • 146
  • 2
    I believe the original question was about "why exactly it happens". – zerkms Apr 26 '19 at 02:13
  • A useful "busy wait" or "spin lock" is actually checking some condition though. There's not really a use for an empty `jmp self`. – JimB Apr 26 '19 at 15:31