25

If I am ranging over a ticker channel and call stop() the channel is stopped but not closed.

In this Example:

package main

import (
    "time"
    "log"
)

func main() {
    ticker := time.NewTicker(1 * time.Second)
    go func(){
        for _ = range ticker.C {
            log.Println("tick")
        }
        log.Println("stopped")
    }()
    time.Sleep(3 * time.Second)
    log.Println("stopping ticker")
    ticker.Stop()
    time.Sleep(3 * time.Second)
}

Running produces:

2013/07/22 14:26:53 tick
2013/07/22 14:26:54 tick
2013/07/22 14:26:55 tick
2013/07/22 14:26:55 stopping ticker

So that goroutine never exits. Is there a better way to handle this case? Should I care that the goroutine never exited?

whatupdave
  • 3,124
  • 5
  • 28
  • 32
  • 11
    Can't close it: "cannot close receive-only channel" – whatupdave Jul 22 '13 at 21:39
  • 3
    The golang docs should really expand on this, but their Ticker example does show how to use a done channel to ensure there's no go-routine leakage: https://golang.org/pkg/time/#example_NewTicker – colm.anseo Mar 27 '18 at 20:18

5 Answers5

23

Used a second channel as Volker suggested. This is what I ended up running with:

package main

import (
    "log"
    "time"
)

// Run the function every tick
// Return false from the func to stop the ticker
func Every(duration time.Duration, work func(time.Time) bool) chan bool {
    ticker := time.NewTicker(duration)
    stop := make(chan bool, 1)

    go func() {
        defer log.Println("ticker stopped")
        for {
            select {
            case time := <-ticker.C:
                if !work(time) {
                    stop <- true
                }
            case <-stop:
                return
            }
        }
    }()

    return stop
}

func main() {
    stop := Every(1*time.Second, func(time.Time) bool {
        log.Println("tick")
        return true
    })

    time.Sleep(3 * time.Second)
    log.Println("stopping ticker")
    stop <- true
    time.Sleep(3 * time.Second)
}
tshepang
  • 12,111
  • 21
  • 91
  • 136
whatupdave
  • 3,124
  • 5
  • 28
  • 32
  • 7
    If your work takes 4 seconds, you'll deadlock your goroutine and it'll be stuck trying to write to the channel it's the only reader of. You really just want a state variable on the `for{}` -- and don't send on the stop channel, just close it. – Dustin Jul 23 '13 at 03:56
  • also `nil` channels are a great method of signaling _after_ a quit operation (`close(quit)`) to any other channel reads: see [nil channels](https://talks.golang.org/2012/10things.slide#13) vs. a [state variable](https://talks.golang.org/2012/10things.slide#12). – colm.anseo Mar 27 '18 at 20:09
15

Signal "done" on a second channel and select in your goroutine between ticker and done channel.

Depending on what you really want to do a better solution might exist, but this is hard to tell from the reduced demo code.

Volker
  • 40,468
  • 7
  • 81
  • 87
8

you can do like this.

package main

import (
    "fmt"
    "time"
)

func startTicker(f func()) chan bool {
    done := make(chan bool, 1)
    go func() {
        ticker := time.NewTicker(time.Second * 1)
        defer ticker.Stop()
        for {
            select {
            case <-ticker.C:
                f()
            case <-done:
                fmt.Println("done")
                return
            }
        }
    }()
    return done
}

func main() {
    done := startTicker(func() {
        fmt.Println("tick...")
    })
    time.Sleep(5 * time.Second)
    close(done)
    time.Sleep(5 * time.Second)
}
ahoLic
  • 171
  • 1
  • 3
0

If you need to save more space, use channels of empty structs (struct{}) which costs no memory. And as mentioned above, don't send something in it - just close, which actually sends zero value.

tshepang
  • 12,111
  • 21
  • 91
  • 136
free2use
  • 311
  • 1
  • 4
  • 14
  • not sure why this was down-voted: `quit := make(chan struct{})` is a great technique and a clear indication of what the channel is being used for i.e. to signal shutdown via `close(quit)` – colm.anseo Mar 27 '18 at 19:54
0

I think this is a nice answer

func doWork(interval time.Duration, work func(), stop chan struct{}, done func()) {
    t := time.NewTicker(interval)
    for {
        select {
        case <-t.C:
            work()
        case _, more := <-stop:
            if !more {
                done()
            }
            return
        }
    }
}

func Test_workTicker(t *testing.T) {

    var wg sync.WaitGroup
    wg.Add(1)
    stop := make(chan struct{})

    go doWork(
        500*time.Millisecond,
        func() { fmt.Println("do work") },
        stop,
        func() {
            fmt.Println("done in go routine")
            wg.Done()
        },
    )

    time.Sleep(5 * time.Second)
    close(stop)
    wg.Wait()
    fmt.Println("done in main")
}
Daniel T
  • 1
  • 2