Тайм-аут для WaitGroup.Wait()

Что такое идиоматический способ назначить тайм-аут WaitGroup.Wait()?

Причина, по которой я хочу это сделать, заключается в том, чтобы защитить мой "планировщик" от потенциального ожидания "рабочего" бегства навсегда. Это приводит к некоторым философским вопросам (например, как система может быть надежно продолжена, если у нее есть странные рабочие?), Но я думаю, что это не подходит для этого вопроса.

У меня есть ответ, который я предоставлю. Теперь, когда я записал это, это выглядит не так плохо, но все еще кажется более запутанным, чем следовало бы. Я хотел бы знать, есть ли что-то доступное, которое проще, более идиоматично или даже альтернативный подход, который не использует WaitGroups.

Та.

Ответ 1

В основном ваше решение, которое вы разместили ниже, так же хорошо, как и может. Несколько советов по его улучшению:

  • В качестве альтернативы вы можете закрыть канал для завершения сигнала вместо отправки на него значения, операция приема на закрытом канале всегда может действовать немедленно.
  • И лучше использовать инструкцию defer для завершения сигнала, она выполняется, даже если функция завершается внезапно.
  • Также, если есть только одно "задание" для ожидания, вы можете полностью опустить WaitGroup и просто отправить значение или закрыть канал после завершения задания (тот же канал, который вы используете в своем заявлении select),.
  • Указание длительности в 1 секунду так же просто, как: timeout := time.Second. Например, укажите 2 секунды: timeout := 2 * time.Second. Вам не требуется преобразование, time.Second уже имеет тип time.Duration, умножая его на нетипизированную константу, подобную 2, также будет введите значение типа time.Duration.

Я бы также создал функцию helper/utility, которая обертывала эту функциональность. Обратите внимание, что 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.

Ответ 2

Я сделал это так: 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")

Это прекрасно работает, но это лучший способ сделать это?

Ответ 3

Я написал библиотеку, которая инкапсулирует логику concurrency 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

Это не реальный ответ на этот вопрос, но я был (гораздо более простым) решением моей маленькой проблемы, когда у меня был этот вопрос.

Мои "рабочие" выполняли запросы 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

Это плохая идея. Не отказывайтесь от подпрограмм, это может привести к гонкам, утечке ресурсов и неожиданным условиям, что в конечном итоге повлияет на стабильность вашего приложения.

Вместо этого постоянно используйте тайм-ауты во всем коде, чтобы убедиться, что никакие процедуры не заблокированы навсегда или слишком долго запускаются.

Идиоматический способ достижения этого - 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(), зная, что он всегда закончится своевременно.