2

I'm studying Go language and I have a question now.

I was just testing buffered and unbuffered channels.

While writing the test code for it, a situation occurred that I couldn't understand the result.

func BenchmarkUnbufferedChannelInt(b *testing.B) {
    ch := make(chan int)
    go func() {
        for i := 0; i < b.N; i++ {
            ch <- i
        }
    }()

    for {
        <-ch
    }
}

The above code doesn't stop, because <-ch makes the goroutine blocked forever. However, the code below does not create an blocked status and the testing succeeds.

func BenchmarkUnbufferedChannelInt(b *testing.B) {
    ch := make(chan int)
    go func() {
        for {
            <-ch
        }
    }()

    for i := 0; i < b.N; i++ {
        ch <- i
    }
}
BenchmarkUnbufferedChannelInt
BenchmarkUnbufferedChannelInt-12         8158381               142.1 ns/op
PASS

As far as I know, there is no parent-child relationship between goroutines, so the termination of one goroutine does not affect the termination of the other goroutines except for main goroutine.

Theoretically, both of the above codes should eventually be blocked forever.

But I don't know why only the second code succeeds.

I wonder how Go calls test functions. Assuming that the goroutine where the test function runs has the same characteristics as the main goroutine, I can understand the result of the above codes.

But if not, I don't understand. In both tests, a goroutine should eventually be blocked forever.

Can you explain the above result? please!

didnlie23
  • 101
  • 7

3 Answers3

4

The above code doesn't stop, because <-ch makes the goroutine blocked forever.

No, it's being blocked because of the infinite for loop.

However, the code below does not create an blocked status and the testing succeeds.

Right, because the for loop runs for as many loops as you want your benchmark to run. Once this loop ends, the goroutine you spun up is forgotten and you have a leak. See https://www.ardanlabs.com/blog/2018/11/goroutine-leaks-the-forgotten-sender.html: If you start a Goroutine that you expect to eventually terminate but it never does then it has leaked.

Maria Ines Parnisari
  • 16,584
  • 9
  • 85
  • 130
  • The created channel is an Unbuffered channel, why does `<-ch` in the for loop not block? If there is no goroutine sending values ​​to the channel, isn't it normal that it should block? – didnlie23 Mar 09 '23 at 02:43
  • I think you're saying the same thing in different words. The for loop has no exit condition (thus is infinite), so it indefinitely blocks on `<-ch` after `b.N` iterations. You should close the channel in the goroutine once finished looping, and you should range over the channel instead of using an infinite for loop. – Gavin Mar 09 '23 at 02:50
  • @Gavin So, the root cause of the first example not exiting is that the producer goroutine exited without closing the channel, so the consumer goroutine never received any data to consume, right? Wouldn't that be because of an infinite loop that wouldn't exit? – didnlie23 Mar 09 '23 at 02:53
  • @Gavin And if I'm right, why doesn't it deadlock and not exit? – didnlie23 Mar 09 '23 at 02:56
  • @didnlie23 No, the root cause is literally a loop with no exit condition. Even if you weren't receiving from the channel in the body of the `for`, you'd still loop forever. You need a way to break out of the `for`. – Gavin Mar 09 '23 at 03:02
  • @Gavin I'm really sorry, but I don't understand. What is the difference between this example code and that test? The result of this example code is a deadlock occurrence. https://go.dev/play/p/Wa8iDFfLh8T – didnlie23 Mar 09 '23 at 03:15
  • 1
    @didnlie23 remove any code inside that for loop and your program will still run forever – Maria Ines Parnisari Mar 09 '23 at 07:24
2

In your second example, the goroutine blocks indefinitely, which is technically a leak but won't block the program itself.

In your first example, you're looping indefinitely outside a goroutine, so the program will block. This isn't specific to Go and isn't really related to receiving from the channel. You're just looping with no exit condition e.g. while (true) {} in about any other language.

Make sure you aren't looping indefinitely in the goroutine to prevent it from leaking. Also, ranging over a channel stops once the channel is emptied and closed, so you can close it in the goroutine once done filling it and range over it instead:

func BenchmarkUnbufferedChannelInt(b *testing.B) {
    ch := make(chan int)
    go func() {
        for i := 0; i < b.N; i++ {
            ch <- i
        }
        close(ch)
    }()

    for _ := range ch {

    }
}
Gavin
  • 4,365
  • 1
  • 18
  • 27
1

I figured out why this happened.

First example: Deadlock occurs because <-ch statement is used even though there is no more data to consume because the producer goroutine iterates more times than the producer goroutine iterates. Even if the consumer goroutine is terminated, the program itself in which the test function is running does not terminate, so deadlock eventually occurs.

Second example: The number of iterations of the consumer goroutine is less than the number of iterations of the producer goroutine. When the loop of the consumer goroutine is finished, the corresponding test function returns and the program ends. At this time, the loop of the producer goroutine has not finished yet, but the program itself has ended, so a goroutine leak occurs.

It took me a long time to understand the situation, thank you all for your replies.

didnlie23
  • 101
  • 7