web-dev-qa-db-ja.com

goの1つでエラーが発生した場合、複数のgoroutineを閉じる

この機能を検討してください:

_func doAllWork() error {
    var wg sync.WaitGroup
    wg.Add(3)
    for i := 0; i < 2; i++ {
        go func() {
            defer wg.Done()
            for j := 0; j < 10; j++ {
                result, err := work(j)
                if err != nil {
                    // can't use `return err` here
                    // what sould I put instead ? 
                    os.Exit(0)
                }
            }
        }()
    }
    wg.Wait()
    return nil
}
_

各ゴルーチンで、関数work()が10回呼び出されます。 work()を1回呼び出すと、実行中のゴルーチンのいずれかでエラーが返された場合、すべてのゴルーチンをすぐに停止し、プログラムを終了します。ここでos.Exit()を使用してもよろしいですか?これをどのように処理する必要がありますか?


編集:この質問は ゴルーチンを停止する方法 とは異なりますここでエラーが発生した場合、すべてのゴルーチンを閉じる必要があります1

13
felix

あなたはこのようなもののために作成された context パッケージを使用することができます( "運送期限、キャンセル信号...")。

context.WithCancel() を使用してキャンセル信号をパブリッシュできるコンテキストを作成します(親コンテキストは context.Background() によって返されるコンテキストである場合があります)。これはcancel()関数を返し、ワーカーゴルーチンへのキャンセル(より正確にはsignalキャンセルインテント)に使用できます。
ワーカールーチンで、Context.Done()によって返されたチャネルが閉じているかどうかを確認することで、そのようなインテントが開始されているかどうかを確認する必要があります。閉じている場合)。また、ノンブロッキングチェックを行うには(閉じていない場合でも続行できるようにするため)、selectステートメントをdefaultブランチとともに使用します。

次のwork()実装を使用します。これは、10%の失敗確率をシミュレートし、1秒の作業をシミュレートします。

_func work(i int) (int, error) {
    if Rand.Intn(100) < 10 { // 10% of failure
        return 0, errors.New("random error")
    }
    time.Sleep(time.Second)
    return 100 + i, nil
}
_

そしてdoAllWork()は次のようになります:

_func doAllWork() error {
    var wg sync.WaitGroup

    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // Make sure it's called to release resources even if no errors

    for i := 0; i < 2; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()

            for j := 0; j < 10; j++ {
                // Check if any error occurred in any other gorouties:
                select {
                case <-ctx.Done():
                    return // Error somewhere, terminate
                default: // Default is must to avoid blocking
                }
                result, err := work(j)
                if err != nil {
                    fmt.Printf("Worker #%d during %d, error: %v\n", i, j, err)
                    cancel()
                    return
                }
                fmt.Printf("Worker #%d finished %d, result: %d.\n", i, j, result)
            }
        }(i)
    }
    wg.Wait()

    return ctx.Err()
}
_

これは、それをテストする方法です。

_func main() {
    Rand.Seed(time.Now().UnixNano() + 1) // +1 'cause Playground's time is fixed
    fmt.Printf("doAllWork: %v\n", doAllWork())
}
_

出力( Go Playground で試してください):

_Worker #0 finished 0, result: 100.
Worker #1 finished 0, result: 100.
Worker #1 finished 1, result: 101.
Worker #0 finished 1, result: 101.
Worker #0 finished 2, result: 102.
Worker #1 finished 2, result: 102.
Worker #1 finished 3, result: 103.
Worker #1 during 4, error: random error
Worker #0 finished 3, result: 103.
doAllWork: context canceled
_

エラーがない場合は、次のwork()関数を使用する場合:

_func work(i int) (int, error) {
    time.Sleep(time.Second)
    return 100 + i, nil
}
_

出力は次のようになります( Go Playground で試してください)。

_Worker #0 finished 0, result: 100.
Worker #1 finished 0, result: 100.
Worker #1 finished 1, result: 101.
Worker #0 finished 1, result: 101.
Worker #0 finished 2, result: 102.
Worker #1 finished 2, result: 102.
Worker #1 finished 3, result: 103.
Worker #0 finished 3, result: 103.
Worker #0 finished 4, result: 104.
Worker #1 finished 4, result: 104.
Worker #1 finished 5, result: 105.
Worker #0 finished 5, result: 105.
Worker #0 finished 6, result: 106.
Worker #1 finished 6, result: 106.
Worker #1 finished 7, result: 107.
Worker #0 finished 7, result: 107.
Worker #0 finished 8, result: 108.
Worker #1 finished 8, result: 108.
Worker #1 finished 9, result: 109.
Worker #0 finished 9, result: 109.
doAllWork: <nil>
_

注:

基本的には、コンテキストのDone()チャネルを使用しただけなので、doneの代わりにContextチャネルを使用することも同じくらい簡単にできると思われます。上記のソリューションでcancel()が行うことを行うためにチャネルを閉じる。

本当じゃない。 これは、1つのゴルーチンだけがチャネルを閉じることができる場合にのみ使用できますが、私たちの場合、いずれかのワーカーが閉じる可能性があります。そして、すでに閉じようとしていますクローズドチャネルパニック(詳細はこちら: 初期化されていないチャネルはどのように動作しますか? )。したがって、close(done)の周りで何らかの同期/除外を確実に行う必要があります。これにより、読みにくくなり、さらに複雑になります。実際、これはcancel()関数が内部で実行する処理であり、目から隠されている/抽象化されているため、cancel()を複数回呼び出して、コードを簡単に使用できるようにすることができます。

ワーカーからエラーを取得して返す方法は?

このため、エラーチャネルを使用できます。

_errs := make(chan error, 2) // Buffer for 2 errors
_

エラーが発生したときにワーカー内で、エラーを出力する代わりにチャネルで送信します。

_result, err := work(j)
if err != nil {
    errs <- fmt.Errorf("Worker #%d during %d, error: %v\n", i, j, err)
    cancel()
    return
}
_

そしてループの後、エラーがあった場合はそれを返します(それ以外の場合はnilを返します):

_// Return (first) error, if any:
if ctx.Err() != nil {
    return <-errs
}
return nil
_

今回の出力(これを Go Playground で試してください):

_Worker #0 finished 0, result: 100.
Worker #1 finished 0, result: 100.
Worker #1 finished 1, result: 101.
Worker #0 finished 1, result: 101.
Worker #0 finished 2, result: 102.
Worker #1 finished 2, result: 102.
Worker #1 finished 3, result: 103.
Worker #0 finished 3, result: 103.
doAllWork: Worker #1 during 4, error: random error
_

ワーカーの数と等しいバッファーサイズのバッファーチャネルを使用したことに注意してください。これにより、送信は常に非ブロッキングになります。これにより、1つだけではなく、すべてのエラーを受信して​​処理することができます(例:最初のエラー)。もう1つのオプションは、バッファチャネルを使用して1のみを保持し、そのチャネルで非ブロッキング送信を行うことです。これは次のようになります。

_errs := make(chan error, 1) // Buffered only for the first error

// ...and inside the worker:

result, err := work(j)
if err != nil {
    // Non-blocking send:
    select {
    case errs <- fmt.Errorf("Worker #%d during %d, error: %v\n", i, j, err):
    default:
    }
    cancel()
    return
}
_
26
icza

ここに行く別の方法は、_errgroup.WithContext_を使用することです。この example で確認できます。

つまり、g.Wait()は、最初のエラーが発生するか、すべてがエラーなしで完了するのを待ちます。ゴルーチンのいずれか(提供されている例ではタイムアウト)でエラーが発生すると、ctx.Done()チャネルを介して他のゴルーチンでの実行がキャンセルされます。

0
eminlala