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
Another solution without leaking wg.Wait()
routine: just use (well-suported and widely-used) golang.org/x/sync/semaphore:
sync.WaitGroup{}
use sem.NewWeighted(N)
(you have to know N
in advance)wg.Add(1)
use err := sem.Acquire(ctx, 1)
defer wg.Done()
use defer sem.Release(1)
wg.Wait()
you can use sem.Acquire(ctx, N)
with context with timeout.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
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?
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
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.
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()
Mostly your solution you posted below is as good as it can get. Couple of tips to improve it:
defer
statement to signal completion, it is executed even if a function terminates abruptly.WaitGroup
and just send a value or close the channel when job is complete (the same channel you use in your select
statement).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.