WaitGroup.Wait()的超时

时间:2015-09-29 09:39:45

标签: go concurrency timeout

WaitGroup.Wait()分配超时的惯用方法是什么?

我想这样做的原因是为了保护我的“日程安排人员”免于永远等待一个错误的“工人”。这导致了一些哲学问题(即一旦有错误的工人,系统如何可靠地继续?),但我认为这个问题超出了这个范围。

我有一个答案,我会提供。现在我已经把它写下来了,它看起来并不那么糟糕,但它仍然感觉比它应该更复杂。我想知道是否有更简单,更惯用的东西,甚至是不使用WaitGroups的替代方法。

的Ta。

9 个答案:

答案 0 :(得分:29)

大多数情况下,您发布的below解决方案尽可能好。一些改进它的技巧:

  • 或者,您可以关闭频道以发信号通知,而不是在其上发送值,即关闭频道can always proceed immediately上的接收操作。
  • 最好使用defer语句来表示完成,即使函数突然终止也会执行它。
  • 如果只有一个"工作"等待,您可以完全省略WaitGroup,只需在作业完成时发送一个值或关闭频道(您在select声明中使用的频道相同)。
  • 指定1秒的持续时间非常简单:timeout := time.Second。例如,指定2秒为:timeout := 2 * time.Second。您不需要转换,time.Second已经是time.Duration类型,将其与类似2的无类型常量相乘也会产生time.Duration类型的值。

我还会创建一个包含此功能的帮助器/实用程序功能。请注意,WaitGroup必须作为指针传递,否则副本将不会被通知" WaitGroup.Done()次来电。类似的东西:

// 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
    }
}

使用它:

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

Go Playground上尝试。

答案 1 :(得分:4)

我是这样做的: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")

它工作正常,但是最好的方法吗?

答案 2 :(得分:2)

大多数现有答案表明泄漏了goroutine。向WaitGroup.Wait分配超时的惯用方式是使用基础的sync/atomic包原语。我从@icza答案中获取了代码,并使用atomic包重写了代码,并添加了上下文取消功能,因为这是通知超时的惯用方式。

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

答案 3 :(得分:0)

我写了一个封装了并发逻辑https://github.com/shomali11/parallelizer的库,你也可以通过它来传递超时。

这是一个没有超时的例子:

func main() {
    group := parallelizer.DefaultGroup()

    group.Add(func() {
        for char := 'a'; char < 'a'+3; char++ {
            fmt.Printf("%c ", char)
        }
    })

    group.Add(func() {
        for number := 1; number < 4; number++ {
            fmt.Printf("%d ", number)
        }
    })

    err := group.Run()

    fmt.Println()
    fmt.Println("Done")
    fmt.Printf("Error: %v", err)
}

输出:

a 1 b 2 c 3 
Done
Error: <nil>

以下是超时的示例:

func main() {
    options := &parallelizer.Options{Timeout: time.Second}
    group := parallelizer.NewGroup(options)

    group.Add(func() {
        time.Sleep(time.Minute)

        for char := 'a'; char < 'a'+3; char++ {
            fmt.Printf("%c ", char)
        }
    })

    group.Add(func() {
        time.Sleep(time.Minute)

        for number := 1; number < 4; number++ {
            fmt.Printf("%d ", number)
        }
    })

    err := group.Run()

    fmt.Println()
    fmt.Println("Done")
    fmt.Printf("Error: %v", err)
}

输出:

Done
Error: timeout

答案 4 :(得分:0)

这不是这个问题的实际答案,但是当我遇到这个问题时,解决了我的小问题(更简单)。

我的'工人'正在做http.Get()请求所以我只是在http客户端设置超时。

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()

答案 5 :(得分:0)

这是一个坏主意。 请勿放弃goroutines ,否则可能会导致竞争,资源泄漏和意外情况,最终影响应用程序的稳定性。

请在整个代码中始终使用超时,以确保不会永远阻塞goroutine或花费太长时间来运行该程序。

实现这一目标的惯用方式是通过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 */ }
  . . .
}()

现在,您可以放心使用WaitGroup.Wait(),因为它将始终可以及时完成。

答案 6 :(得分:0)

另一种不泄漏 wg.Wait() 例程的解决方案:只需使用(得到良好支持和广泛使用)golang.org/x/sync/semaphore

  • 使用sync.WaitGroup{}代替sem.NewWeighted(N)(您必须提前知道N
  • 使用 wg.Add(1) 代替 err := sem.Acquire(ctx, 1)
  • 使用 defer wg.Done() 代替 defer sem.Release(1)
  • 您可以将 wg.Wait() 与带超时的上下文一起使用,而不是 sem.Acquire(ctx, N)
  • 注意,这仅相当于此特定用例中的 sync.WaitGroup(当您只调用 Add(1)Release(1) N 次时)。仔细阅读文档。

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")
}

结果:

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

答案 7 :(得分:0)

提出一个不会泄漏 goroutine 或依赖轮询(睡眠)的解决方案:

const increaseEndDateFinish = (e, idx) => {
  const target = e.target;
  target.style.width = '200px';
  console.log(target.clientWidth);
};

用法:

import "atomic"

type WaitGroup struct {
    count int32
    done chan struct{}
}

func NewWaitGroup() *WaitGroup {
    return &WaitGroup{
        done: make(chan struct{}),
    }
}

func (wg *WaitGroup) Add(i int32) {
    select {
    case <-wg.done:
        panic("use of an already closed WaitGroup")
    default:
    }
    atomic.AddInt32(&wg.count, i)
}

func (wg *WaitGroup) Done() {
    i := atomic.AddInt32(&wg.count, -1)
    if i == 0 {
        close(wg.done)
    }
    if i < 0 {
        panic("too many Done() calls")
    }
}

func (wg *WaitGroup) C() <-chan struct{} {
    return wg.done
}

答案 8 :(得分:0)

我们对我们的一个系统也有同样的需求。通过将上下文传递给 goroutines 并在我们面临超时时关闭该上下文,我们将防止 goroutine 泄漏。

func main() {
    ctx := context.Background()
    ctxWithCancel, cancelFunc := context.WithCancel(ctx)
    var wg sync.WaitGroup
    Provide(ctxWithCancel, 5, &wg)
    Provide(ctxWithCancel, 5, &wg)
    c := make(chan struct{})
    go func() {
        wg.Wait()
        c <- struct{}{}
        fmt.Println("closed")
    }()

    select {
    case <-c:
    case <-time.After(20 * time.Millisecond):
        cancelFunc()
        fmt.Println("timeout")
    }
}

func Work(ctx context.Context, to int) {
    for i := 0; i < to; i++ {
        select {
        case <-ctx.Done():
            return
        default:
            fmt.Println(i)
            time.Sleep(10 * time.Millisecond)
        }
    }
}

func Provide(ctx context.Context, to int, wg *sync.WaitGroup) {
    wg.Add(1)
    go func() {
        Work(ctx, to)
        wg.Done()
    }()
}