Timeout for WaitGroup.Wait()

前端 未结 7 805
清歌不尽
清歌不尽 2021-02-01 04:33

What is an idiomatic way to assign a timeout to WaitGroup.Wait() ?

The reason I want to do this, is to safeguard my \'scheduler\' from potentially awaiting an errant \'w

相关标签:
7条回答
  • 2021-02-01 04:55

    Another solution without leaking wg.Wait() routine: just use (well-suported and widely-used) golang.org/x/sync/semaphore:

    • Instead of sync.WaitGroup{} use sem.NewWeighted(N) (you have to know N in advance)
    • Instead of wg.Add(1) use err := sem.Acquire(ctx, 1)
    • Instead of defer wg.Done() use defer sem.Release(1)
    • Instead of wg.Wait() you can use sem.Acquire(ctx, N) with context with timeout.
    • Watch out, this is only equivalent to sync.WaitGroup in this specific use-case (when you only call Add(1) and Release(1) N times). Read the documentation carefully.

    Example:

    package main
    
    import (
        "context"
        "log"
        "time"
    
        "golang.org/x/sync/semaphore"
    )
    
    func worker(n int) {
        time.Sleep(time.Duration(n) * time.Second)
        log.Printf("Worker %v finished", n)
    }
    
    func main() {
    
        const N = 5
        sem := semaphore.NewWeighted(N)
    
        for i := 0; i < N; i++ {
    
            err := sem.Acquire(context.Background(), 1)
            if err != nil {
                log.Fatal("sem.Acquire err", err)
            }
            go func(n int) {
                defer sem.Release(1)
                worker(n)
            }(i)
        }
    
        ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
        defer cancel()
    
        err := sem.Acquire(ctx, N)
        if err != nil {
            log.Println("sem.Acquire err:", err)
            return
        }
    
        log.Println("sem.Acquire ok")
    }
    

    Which results in:

    2009/11/10 23:00:00 Worker 0 finished
    2009/11/10 23:00:01 Worker 1 finished
    2009/11/10 23:00:02 Worker 2 finished
    2009/11/10 23:00:02 sem.Acquire err: context deadline exceeded
    
    0 讨论(0)
  • 2021-02-01 05:01

    I did it like this: http://play.golang.org/p/eWv0fRlLEC

    go func() {
        wg.Wait()
        c <- struct{}{}
    }()
    timeout := time.Duration(1) * time.Second
    fmt.Printf("Wait for waitgroup (up to %s)\n", timeout)
    select {
    case <-c:
        fmt.Printf("Wait group finished\n")
    case <-time.After(timeout):
        fmt.Printf("Timed out waiting for wait group\n")
    }
    fmt.Printf("Free at last\n")
    

    It works fine, but is it the best way to do it?

    0 讨论(0)
  • 2021-02-01 05:01

    Most existing answers suggest leaking goroutines. The idiomatic way to assign a timeout to WaitGroup.Wait is to use underlying sync/atomic package primitives. I took code from @icza answer and rewrote it using the atomic package, and added context cancelation as that's an idiomatic way to notify of a timeout.

    package main
    
    import (
        "context"
        "fmt"
        "sync/atomic"
        "time"
    )
    
    func main() {
        var submitCount int32
        // run this instead of wg.Add(1)
        atomic.AddInt32(&submitCount, 1)
    
        // run this instead of wg.Done()
        // atomic.AddInt32(&submitCount, -1)
    
        timeout := time.Second
        ctx, cancel := context.WithTimeout(context.Background(), timeout)
        defer cancel()
        fmt.Printf("Wait for waitgroup (up to %s)\n", timeout)
    
        waitWithCtx(ctx, &submitCount)
    
        fmt.Println("Free at last")
    }
    
    // waitWithCtx returns when passed counter drops to zero
    // or when context is cancelled
    func waitWithCtx(ctx context.Context, counter *int32) {
        ticker := time.NewTicker(10 * time.Millisecond)
        for {
            select {
            case <-ctx.Done():
                return
            case <-ticker.C:
                if atomic.LoadInt32(counter) == 0 {
                    return
                }
            }
        }
    }
    

    Same code in Go Playground

    0 讨论(0)
  • 2021-02-01 05:05

    This is a bad idea. Do not abandon goroutines, doing so may introduce races, resource leaks and unexpected conditions, ultimately impacting the stability of your application.

    Instead use timeouts throughout your code consistently in order to make sure no goroutine is blocked forever or takes too long to run.

    The idiomatic way for achieving that is via context.WithTimeout():

    ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second)
    defer cancel()
    
    // Now perform any I/O using the given ctx:
    go func() {
      err = example.Connect(ctx)
      if err != nil { /* handle err and exit goroutine */ }
      . . .
    }()
    

    Now you can safely use WaitGroup.Wait(), knowing it will always finish in a timely manner.

    0 讨论(0)
  • 2021-02-01 05:08

    This is not an actual answer to this question but was the (much simpler) solution to my little problem when I had this question.

    My 'workers' were doing http.Get() requests so I just set the timeout on the http client.

    urls := []string{"http://1.jpg", "http://2.jpg"}
    wg := &sync.WaitGroup{}
    for _, url := range urls {
        wg.Add(1)
        go func(url string) {
            client := http.Client{
                Timeout: time.Duration(3 * time.Second), // only want very fast responses
            }
            resp, err := client.Get(url)
            //... check for errors
            //... do something with the image when there are no errors
            //...
    
            wg.Done()
        }(url)
    
    }
    wg.Wait()
    
    0 讨论(0)
  • 2021-02-01 05:12

    Mostly your solution you posted below is as good as it can get. Couple of tips to improve it:

    • Alternatively you may close the channel to signal completion instead of sending a value on it, a receive operation on a closed channel can always proceed immediately.
    • And it's better to use defer statement to signal completion, it is executed even if a function terminates abruptly.
    • Also if there is only one "job" to wait for, you can completely omit the WaitGroup and just send a value or close the channel when job is complete (the same channel you use in your select statement).
    • Specifying 1 second duration is as simple as: timeout := time.Second. Specifying 2 seconds for example is: timeout := 2 * time.Second. You don't need the conversion, time.Second is already of type time.Duration, multiplying it with an untyped constant like 2 will also yield a value of type time.Duration.

    I would also create a helper / utility function wrapping this functionality. Note that WaitGroup must be passed as a pointer else the copy will not get "notified" of the WaitGroup.Done() calls. Something like:

    // waitTimeout waits for the waitgroup for the specified max timeout.
    // Returns true if waiting timed out.
    func waitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
        c := make(chan struct{})
        go func() {
            defer close(c)
            wg.Wait()
        }()
        select {
        case <-c:
            return false // completed normally
        case <-time.After(timeout):
            return true // timed out
        }
    }
    

    Using it:

    if waitTimeout(&wg, time.Second) {
        fmt.Println("Timed out waiting for wait group")
    } else {
        fmt.Println("Wait group finished")
    }
    

    Try it on the Go Playground.

    0 讨论(0)
提交回复
热议问题