2016-11-25 5 views
2

私は単純な同時実行ユースケースをgoで持っています。どんな助けもありがとう。慣用的なゴルーチンの終了とエラー処理

リモートサーバーから不特定多数のリソースに並列にクエリを送信する方法fetchAllを書きたいと思います。フェッチのいずれかが失敗した場合、その最初のエラーを直ちに返したいと思います。私は他のスレッドのクリーンアップに信号チャネルを作成することができhttps://blog.golang.org/pipelinesを読んでからhttps://play.golang.org/p/Be93J514R5

私が知っている:

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()) 
} 

遊び場:

私の最初の、素朴な実装では、ゴルーチンをリーク。あるいは、私はおそらくそれを達成するためにcontextを使用することができます。しかし、このようなシンプルなユースケースには、私が見逃している簡単な解決策があるはずです。

答えて

4

あなたのゴルーチンのうち1つを除くすべてが、errsチャネルに送信するのをまだ待っているため、リークします。空いている範囲を決して終了しません。ウェイトグループは決して終了しないので、あなたはまた、errsチャネルを閉じることであるゴルーチンをリークしています。

は(アンディが指摘したようにそれはミューテックスからの保護を必要とすると思いますので、また、マップから削除すると、スレッドセーフではありません。)

しかし、私は地図、ミューテックス、waitgroups、文脈を考えていませんここでも必要です。

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

EDIT:確かに、それを指摘してくれてありがとう愚かな間違いがあった私は、次のようなものを基本的なチャンネル操作を使用して全体を書き換えると思います。上記のコードを修正しました(私はそう思います...)。私はまた、追加されたRealism™のランダム性を追加しました。

また、私はこの問題に近づくには複数の方法がありますが、私のソリューションは1つの方法だと強調したいと思います。最終的には個人的な好みになりますが、一般的には、「慣用的」なコードに向かって努力したいと思います。

+0

'ec:= chan error(nil)'は面白いです、前にこのパターンを見たことがありません。私は 'select'原因がランダムな順序で実行されたと考えました。'done <-true'が' ec <-err'の前に送信される競合状態はありますか? – gerad

+0

良いキャッチは、絶対にレースです!私は急いで、私が言及したように、それを試していないと書いています(あなたはいつもそうするべきです)。幸いなことに、間違いを修正するだけで、コード全体がより簡単になります。この場合、 'chan error(nil)'トリックは必要ありません(select文からの送信をブロックして、複数の条件付き選択を書く必要がありません)。私の間違いを指摘してくれてありがとう:) – Aedolon

+0

これはさらに簡素化することができます。個別の完了チャネルとエラーチャネルは必要ありません。改善する以外にもいくつかの点があります。 https://play.golang.org/p/1a0ZXuy3Dz –

0

各ゴルーチンが完了する限り、何も漏れません。チャネル上の送信操作がブロックされないように、ゴルーチンの数に等しいバッファサイズでバッファされたエラーチャネルを作成する必要があります。各ゴルーチンは、成功するか失敗するかに関係なく、終了時に常にチャンネル上に何かを送るべきです。一番下のループは、ゴルーチンの数だけ反復して、無限のエラーを返すことができます。チャンネルを閉じるWaitGroupや他のゴルーチンは必要ありません。

私は、ゴルーチンが漏れているように見える理由は、最初のエラーが発生したときに戻るので、まだ実行中のものがあると思います。

ところで、地図はゴルーチンの安全ではありません。ゴルーチン間でマップを共有していて、マップの一部を変更している場合は、そのマップをmutexで保護する必要があります。

+0

私は、バッファリングされたチャネルを使用すると動作することに同意します。フェッチ数は事前にわかりませんが(実際のコードは例よりも複雑ですが)、その解決策を避けようとしていました。 – gerad

関連する問題