0
votes
func startTimer(ctx context.Context, intervalTime int) {
    intervalChan := make(chan bool) 
    go func() {
        for {
            select {
            case <-ctx.Done():
                return
            case <-time.After(time.Second * time.Duration(intervalTime)):
                intervalChan <- true
            }
        }
    }()


    for {
        select {
        case <-ctx.Done():
            return
        case <-intervalChan:
            doSomething()
    }
}

Hi,I write a func as above and want to know is it possible to cause goroutine leak.

For example, the first select statement sends a true to intervalChan, then the second select statement receives Done flag from ctx.Done() and return. Will the goroutine be block forever?

2
do you cancel your context with defer.. 3 paragraph in the documentation golang.org/pkg/context.. also why use intervalChan its much cleaner to implement a context with timeout since you're already using a context and drop intervalChan golang.org/pkg/context/#example_WithTimeout - reticentroot
hi @reticentroot, in my origin program, context is used for cancelling the timer manually. And using the timeout of context may not be functionally the same with my func - Levey

2 Answers

1
votes

I cannot replicate this behaviour every time but could be some leak. If doSomething do some heavy computation, meanwhile goroutine is blocked on intervalChan <- true since it cannot push into the channel. After doSomething finish execution and context was cancelled, startTimer exists before goroutine and this will lead into blocked goroutine, because there isn't any consumer of intervalChan.

go version go1.8.3 darwin/amd64

package main

import (
    "context"
    "fmt"
    "time"
)

func startTimer(ctx context.Context, intervalTime int) {
    intervalChan := make(chan bool)
    go func() {
        for {
            select {
            case <-ctx.Done():
                fmt.Println("Done from inside of goroutine.")
                return
            case <-time.After(time.Second * time.Duration(intervalTime)):
                fmt.Println("Interval reached.")
                intervalChan <- true
            }
        }
    }()

    for {
        select {
        case <-ctx.Done():
            fmt.Println("Done from startTimer.")
            return
        case <-intervalChan:
            time.Sleep(10 * time.Second)
            fmt.Println("Done")
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    startTimer(ctx, 2)
}
1
votes

The only place your first goroutine could be blocked indefinitely is in intervalChan <- true. Put it in another select block to be able to cancel that send:

go func() {
    for {
        select {
        case <-ctx.Done():
            return
        case <-time.After(time.Second * time.Duration(intervalTime)):
            select {
            case <-ctx.Done():
                return
            case intervalChan <- true:
            }
        }
    }
}()