1

I ran into the next piece of code (simplified) and can't understand purpose of channel check. This snippet was found right before a function end.

check := make(chan struct{}, 1)
go func() {
    check <- struct{}{}
    q.reader()
}()
<-check

return nil

What would happen if check had zero capacity, i.e. created as check := make(chan struct{}) ?

What's the difference if I simply remove the signal-only channel and write the following? I guess it does nothing.

go q.reader()
return nil
6
  • I think you've over-simplified the example, while your check channel does something, it's not particularly useful. The only thing it ensures is that the send on check happened before the outer function returns, which serves no purpose. If this is the extent of the code, then yes, there's no real reason for the channel. Commented Sep 18, 2024 at 13:42
  • Well, check is a local variable and is not passed outside the function I'm scrutinizing. Identifier check appears 3 times in the function body (the definition, inside the goroutine, and in the receive operator). @JimB, thank you for confirming my guess, the channel is no use. I confused this with some kind of Go idiom. Commented Sep 18, 2024 at 14:04
  • can you give more context around this code ? for example: is it actual production code ? or code in a testing function ? Commented Sep 18, 2024 at 14:17
  • 2
    I’ve seen similar things where someone tries to check that a goroutine has “started”, but that of course doesn’t mean anything if the code in the goroutine isn’t synchronized in any way. Commented Sep 18, 2024 at 14:28
  • @LeGEC, this is production code. At first, I made changes in the NewQueue constructor (paste.debian.net/hidden/a954e246, q.wg is sync.WaitGroup). But afterwards I encountered similar pattern in test code (paste.debian.net/hidden/689277d9), and I began to doubt. Commented Sep 18, 2024 at 14:35

2 Answers 2

0

It's hard to predict what would happen if the channel were unbufered, but let's assume the worst-case scenario, here's the order in which the statements could be executed:

check := make(chan struct{})
// routine starts immediately after the channel is created
check <- struct{}{} // write is blocking, because nothing is reading the channel
 <-check // once this is executed, the routine above continues
return nil // at this point, we don't know whether or not 
q.reader() // may or may not be called, it may not have returned, we don't know

Now if you called this function from main(), and your program terminated, you may or may not have called, or may not have returned. We can't know for sure. Equally possible is that this will happen:

check := make(chan struct{})
<-check
check <- struct{}{}
q.reader()
return nil

But the return and q.reader() call can swap places, and there is still no guarantee q.reader has returned.

By contrast, having the buffered channel at least ensures that by the time we get past <-check, the routine has started, and most likely we have called q.reader(). If q.reader() takes a while to return, though, we still don't know whether or not q.reader returned. If that matters, it might be preferable to do something like:

check := make(chan struct{}) // note, unbuffered
go func() {
    q.reader()
    close(check)
}()
<-check // wait for the channel to be closed, indicating our routine has done its job
return nil

If your channel is meant to signal that the routine has started, then a buffered channel is preferable, because writing to the channel isn't going to block the routine, so you could do something like this:

ch := make(chan struct{}, 1)
go func() {
    ch <- struct{}{} // indicate routine has started
    q.reader()
    close(ch)
}()
<-ch // wait for the routine to start, it may have started already
<-ch // wait for the channel to be closed, indicating the routine has done its job
return nil

At this point, though, it's easier to use a waitgroup, but if you want to log output from the routine (say an error) you may want to use something like this:

ch := make(chan error, 1)
go func() {
    var err error
    defer func() {
        if err != nil {
            ch <-err // communicate the error
        }
        close(ch) // close the channel, the routine is done
    }()
    if q == nil {
        ch <- errors.Errorf("nothing to work with, creating the object with defaults") // non-fatal error
        q = newQ()
    }
    if err = q.reader(); err != nil {
        return // bail
    }
    if err = q.another(); err != nil {
        return
    }
    // add as many calls as you like
}()
// keep reading from the channel until closed
for err := range ch {
    if err != nil {
        fmt.Printf("ERROR: %v\n", err)
    }
}
// once the channel is closed, we're all done
return nil

With this, you can have a routine that sends back errors over the channel, so you can log what's going on. depending on how many non-fatal errors, you can increase the buffer of the channel, to ensure that the calls that would cause the routine to exit early are never blocked. In the example, there's just one non-fatal error, so the channel has a buffer of size 1. That way, I'm guaranteed that the routine will reach q.reader() no matter whether the routine where I'm reading from the channel is ready or not.


What is the channel used for?

Put simply, this is a way to check whether a routine has started, no more no less. It does not guarantee the routine has completed, though. That's when waitgroups are far more useful. Situations where channels like this are more useful is when handling signals:

app, cfunc := NewApp() // cfunc cancels the application context (aka shutdown)
appCtx := app.Context() // get the context shared by the entire application
sCh := make(chan os.Signal, 1)
defer close(sCh)
// start listening for signals
signal.Notify(sCh, syscall.SIGTERM, syscall.SIGINT)
app.Run() // start the application
// wait either for a signal, or the app context is cancelled
for {
    select {
    case sig := <-sCh:
        log.Warnf("Received signal %+v\n", sig)
        cfunc() // shut down the application
        return
    case <-appCtx.Done():
        log.Info("App shutting down")
        return
    }
}

Here, the channel for the signal must be buffered, or else we risk missing the signal as per the docs:

// Set up channel on which to send signal notifications.
// We must use a buffered channel or risk missing the signal
// if we're not ready to receive when the signal is sent.
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)

When is the code as presented useful?

It isn't really, unless you have some kind of test suite where you want to mimic data being provided/ingested from an external source (something like a queue), and you want to know when your routine that will be sending data has started. You'll likely want some way to know whether or not your routine has returned, or some other mechanism to stop it, but that's a different matter.

Sign up to request clarification or add additional context in comments.

6 Comments

"this is a way to check whether a routine has started", it isn't really, because there's no specification for what "started" means. A goroutine might "start" immediately on invocation of the go keyword, or not, and it might stop and need to be restarted later, it's all the same as far as the language is concerned. The only thing it checks is whether the channel send operation happened, nothing more.
Perhaps I phrased it poorly. If we reach the send statement, we do know that the anonymous function invoked as a separate routine has been invoked, and its first instruction has been executed, in that sense, the routine "has started" ie, one of its statements has been executed. What happens after that, whether we reach the second statement or not, whether that call returns or not, we don't know
I was trying to emphasize that semantically "invoked", "started",or whatever here has no practical meaning. There is no observable side effect of when that stack gets initialized or when the program-counter address is at the start of the function body. The send could be even completed and the goroutine put back to sleep again, effectively meaning the goroutine is no longer started and in no different a state than before the channel send. Maybe I've just seen to many people try to "check whether a goroutine has started" which is completely the wrong thing to try and solve in the first place.
I understood what you meant. The only thing that a send on the channel tells you, if you read it, is that at some point, the routine sent something on the channel. yes, the routine may be asleep, it may have returned, there's no telling what its state is, other than at some point, it wrote the the channel, it's also why I wrote that bit at the end of my answer about the code as presented not being useful. A write at the end of a routine, after something important is done is more significant, but then you'd probably use a waitgroup etc...
Oh yes, I know you understood! That was for the people that are inclined to fixate on the incorrect problem statement ;)
|
0

As @JimB explained in comment, the check channel (of capacity either one or zero) has no real meaning. Unless the signal-only channel is used in a way after q.reader() call, by sending an empty struct or closing the channel, it does not affect semantic of the program and can be removed without consequences. There is no strong guarantee on q.reader() is running before return nil.

An important thing we need to know about Go, concurrency does not necessarily imply parallelism, and the specification says nothing about parallel execution. Watch Rob Pike's talk to better understand the deference → https://go.dev/blog/waza-talk

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.