web-dev-qa-db-ja.com

慣用的なgoroutineの終了とエラー処理

私は単純な同時実行のユースケースを使用していますが、エレガントなソリューションを理解することができません。任意の助けいただければ幸いです。

リモートサーバーから指定されていない数のリソースを並列にクエリするメソッドfetchAllを記述したいと思います。フェッチのいずれかが失敗した場合、その最初のエラーをすぐに返します。

私の最初の素朴な実装は、ゴルーチンをリークします:

package main

import (
  "fmt"
  "math/Rand"
  "sync"
  "time"
)

func fetchAll() error {
  wg := sync.WaitGroup{}
  errs := make(chan error)
  leaks := make(map[int]struct{})
  defer fmt.Println("these goroutines leaked:", leaks)

  // run all the http requests in parallel
  for i := 0; i < 4; i++ {
    leaks[i] = struct{}{}
    wg.Add(1)
    go func(i int) {
      defer wg.Done()
      defer delete(leaks, i)

      // pretend this does an http request and returns an error
      time.Sleep(time.Duration(Rand.Intn(100)) * time.Millisecond)
      errs <- fmt.Errorf("goroutine %d's error returned", i)
    }(i)
  }

  // wait until all the fetches are done and close the error
  // channel so the loop below terminates
  go func() {
    wg.Wait()
    close(errs)
  }()

  // return the first error
  for err := range errs {
    if err != nil {
      return err
    }
  }

  return nil
}

func main() {
  fmt.Println(fetchAll())
}

遊び場: https://play.golang.org/p/Be93J514R5

https://blog.golang.org/pipelines を読むことで、他のスレッドをクリーンアップするシグナルチャネルを作成できることがわかります。あるいは、contextを使用して実現することもできます。しかし、そのような単純なユースケースには、私が見逃しているより単純なソリューションがあるはずです。

15
gerad

1つを除くすべてのゴルーチンがerrsチャネルへの送信を待機しているため、すべてがリークされます。空のfor-rangeを終了することはありません。また、ウェイトグループが終了しないため、errsチャネルを閉じるのがゴルーティンのリークです。

(また、Andyが指摘したように、マップからの削除はスレッドセーフではないため、ミューテックスからの保護が必要です。)

ただし、ここではマップ、ミューテックス、ウェイトグループ、コンテキストなどは必要ないと思います。次のような基本的なチャネル操作のみを使用するように全体を書き直します。

package main

import (
    "fmt"
    "math/Rand"
    "time"
)

func fetchAll() error {
    var N = 4
    quit := make(chan bool)
    errc := make(chan error)
    done := make(chan error)
    for i := 0; i < N; i++ {
        go func(i int) {
            // dummy fetch
            time.Sleep(time.Duration(Rand.Intn(100)) * time.Millisecond)
            err := error(nil)
            if Rand.Intn(2) == 0 {
                err = fmt.Errorf("goroutine %d's error returned", i)
            }
            ch := done // we'll send to done if nil error and to errc otherwise
            if err != nil {
                ch = errc
            }
            select {
            case ch <- err:
                return
            case <-quit:
                return
            }
        }(i)
    }
    count := 0
    for {
        select {
        case err := <-errc:
            close(quit)
            return err
        case <-done:
            count++
            if count == N {
                return nil // got all N signals, so there was no error
            }
        }
    }
}

func main() {
    Rand.Seed(time.Now().UnixNano())
    fmt.Println(fetchAll())
}

遊び場のリンク: https://play.golang.org/p/mxGhSYYkOb

編集:実際に愚かな間違いがありました、指摘してくれてありがとう。上記のコードを修正しました(そう思います...)。また、追加されたRealism™にランダム性を追加しました。

また、この問題に対処するにはいくつかの方法があり、私の解決策は1つの方法にすぎないことを強調しておきます。最終的には個人的な好みに帰着しますが、一般的には、「慣用的な」コードに向けて、そして自然でわかりやすいスタイルに向けて努力したいと考えています。

15
LemurFromTheId

Error Group を使用すると、これがさらに簡単になります。これは、提供されたすべてのGoルーチンが正常に完了するまで自動的に待機するか、いずれかのルーチンがエラーを返した場合に残っているルーチンをすべてキャンセルします(この場合、エラーは呼び出し側に戻る1つのバブルです)。

package main

import (
        "context"
        "fmt"
        "math/Rand"
        "time"

        "golang.org/x/sync/errgroup"
)

func fetchAll(ctx context.Context) error {
        errs, ctx := errgroup.WithContext(ctx)

        // run all the http requests in parallel
        for i := 0; i < 4; i++ {
                errs.Go(func() error {
                        // pretend this does an http request and returns an error                                                  
                        time.Sleep(time.Duration(Rand.Intn(100)) * time.Millisecond)                                               
                        return fmt.Errorf("goroutine %d's error returned", i)                                                      
                })
        }

        // Wait for completion and return the first error (if any)                                                                 
        return errs.Wait()
}

func main() {
        fmt.Println(fetchAll(context.Background()))
}
20
joth

各ゴルーチンが完了する限り、何もリークしません。エラーチャネルは、ゴルーチンの数と等しいバッファサイズでバッファリングされた状態で作成して、チャネルでの送信操作がブロックされないようにする必要があります。各goroutineは、成功した場合も失敗した場合も、終了時に常に何かをチャネルに送信する必要があります。最下部のループは、ゴルーチンの数だけ反復して、nil以外のエラーが発生した場合に戻ることができます。 WaitGroupや、チャネルを閉じる他のgoroutineは必要ありません。

Goroutineがリークしているように見える理由は、最初のエラーが発生したときに戻るため、それらのいくつかはまだ実行されているためです。

ちなみに、地図は安全ではありません。ゴルーチン間でマップを共有していて、それらのいくつかがマップに変更を加えている場合は、ミューテックスで保護する必要があります。

2
Andy Schweig

joth によって提案された errgroup を使用したより完全な例を次に示します。成功したデータの処理を示し、最初のエラーで終了します。

https://play.golang.org/p/rU1v-Mp2ijo

package main

import (
    "context"
    "fmt"
    "golang.org/x/sync/errgroup"
    "math/Rand"
    "time"
)

func fetchAll() error {
    g, ctx := errgroup.WithContext(context.Background())
    results := make(chan int)
    for i := 0; i < 4; i++ {
        current := i
        g.Go(func() error {
            // Simulate delay with random errors.
            time.Sleep(time.Duration(Rand.Intn(100)) * time.Millisecond)
            if Rand.Intn(2) == 0 {
                return fmt.Errorf("goroutine %d's error returned", current)
            }
            // Pass processed data to channel, or receive a context completion.
            select {
            case results <- current:
                return nil
            // Close out if another error occurs.
            case <-ctx.Done():
                return ctx.Err()
            }
        })
    }

    // Elegant way to close out the channel when the first error occurs or
    // when processing is successful.
    go func() {
        g.Wait()
        close(results)
    }()

    for result := range results {
        fmt.Println("processed", result)
    }

    // Wait for all fetches to complete.
    return g.Wait()
}

func main() {
    fmt.Println(fetchAll())
}

2
syvex