web-dev-qa-db-ja.com

WaitGroup.Wait()のタイムアウト

WaitGroup.Wait() にタイムアウトを割り当てる慣用的な方法は何ですか?

私がこれをしたい理由は、私の「スケジューラ」が誤った「ワーカー」を永遠に待つことを防ぐためです。これはいくつかの哲学的な質問につながります(つまり、誤った労働者がいたら、システムはどのようにして確実に継続できますか?)が、この質問の範囲外だと思います。

私が提供する答えがあります。書き留めたので、それほど悪くはないように見えますが、それでも、必要以上に複雑に感じられます。より簡単で、より慣用的なもの、またはWaitGroupsを使用しない別のアプローチで利用できるものがあるかどうか知りたいのですが。

た。

21
laher

主にあなたが投稿したあなたの解決策 はそれが得ることができるのと同じくらい良いです。それを改善するためのいくつかのヒント:

  • または、値を送信する代わりにチャネルを閉じて完了を通知することもできます。閉じたチャネルでの受信操作 常にすぐに続行できます
  • また、deferステートメントを使用して完了を通知することをお勧めします。これは、関数が突然終了した場合でも実行されます。
  • また、待機する「ジョブ」が1つしかない場合は、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 で試してください。

43
icza

私はそれをこのようにしました: 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")

それはうまくいきますが、それはそれを行うための最良の方法ですか

7
laher

これはこの質問に対する実際の回答ではありませんが、この質問があったときの私の小さな問題の(はるかに単純な)解決策でした。

私の「ワーカー」は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()
1
JtotheR

ほとんどの既存の回答は、ゴルーチンのリークを示唆しています。 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, _ := context.WithTimeout(context.Background(), timeout)
    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
            }
        }
    }
}

Go Playgroundの同じコード

0

これは悪い考えです。 goroutinesを破棄しないでください。破棄すると、競合、リソースリーク、予期しない状態が発生し、最終的にアプリケーションの安定性に影響を与える可能性があります。

代わりに、コード全体でタイムアウトを一貫して使用して、ゴルーチンが永久にブロックされたり、実行に時間がかかりすぎたりしないようにします。

これを実現する慣用的な方法は 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()を安全に使用できるようになりました。常にタイムリーに終了することがわかっています。

0
rustyx

同時実行ロジックをカプセル化するライブラリを作成しました 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
0
Raed Shomali