Search code examples
goconcurrencytimeoutchannel

Break out of loop when no channel in a select group receives signal in specified time


How do I break out of the idiomatic Go for loop containing select statement, if and only if I receive no signals on any of the channels my select statement is listening to, for a particular period of time.

Let me enhance the question with an example.

Setup:

  1. Lets say that i have a channel var listenCh <-chan string that I am listening to.
  2. Let us assume that some other go routine(not in our control) sends different strings on this channel.
  3. I do some processing with the given string and then listen for the next string on the listenCh.

Requirement:

I want to wait 10 seconds maximum(precision not critical), between two successive signals on the listenCh, before I shut down my operations(break out of for loop permanently).

Code Stub:

func doingSomething(listenCh <-chan string) {
  var mystr string
  for {
    select {
      case mystr <-listenCh:
        //dosomething
      case /*more than 10 seconds since last signal on listenCh*/:
        return
    }
  }
}

How would I achieve my requirement in the most efficient manner possible.

The usual quit channel technique with time.After(time.Duration) seems to not reset after one loop and hence the whole program closes in 10 seconds even if there is a continuous stream of values.

I find variants of the question(but not what I want) on SO, but none that I saw answers my particular use case.


Solution

  • Foreword: Using time.Timer is the recommended way, use of time.After() here is only for demonstration and reasoning. Please use the 2nd approach.


    Using time.After() (not recommended for this)

    If you put time.After() in the case branch, that will "reset" in each iteration, because that will return you a new channel each time, so that works:

    func doingSomething(listenCh <-chan string) {
        for {
            select {
            case mystr := <-listenCh:
                log.Println("Received", mystr)
            case <-time.After(1 * time.Second):
                log.Println("Timeout")
                return
            }
        }
    }
    

    (I used 1 second timeout for testability on the Go Playground.)

    We can test it like:

    ch := make(chan string)
    go func() {
        for i := 0; i < 3; i++ {
            ch <- fmt.Sprint(i)
            time.Sleep(500 * time.Millisecond)
        }
    }()
    doingSomething(ch)
    

    Output (try it on the Go Playground):

    2009/11/10 23:00:00 Received 0
    2009/11/10 23:00:00 Received 1
    2009/11/10 23:00:01 Received 2
    2009/11/10 23:00:02 Timeout
    

    Using time.Timer (recommended solution)

    If there is a high rate receiving from the channel, this might be a bit of wasting resources, as a new timer is created and used under the hood by time.After(), which doesn't magically stop and get garbage collected immediately when it's not needed anymore in case you receive a value from the channel before timeout.

    A more resource-friendly solution would be to create a time.Timer before the loop, and reset it if a value is received before a timeout.

    This is how it would look like:

    func doingSomething(listenCh <-chan string) {
        d := 1 * time.Second
        t := time.NewTimer(d)
        for {
            select {
            case mystr := <-listenCh:
                log.Println("Received", mystr)
                if !t.Stop() {
                    <-t.C
                }
                t.Reset(d)
            case <-t.C:
                log.Println("Timeout")
                return
            }
        }
    }
    

    Testing and output is the same. Try this one on the Go Playground.