時間を使わずにすべてのゴルーチンが完了するのを待つ方法スリープ?


108

このコードは、同じフォルダー内のすべてのxmlファイルを選択します。呼び出された実行可能ファイルがコールバックメソッドの各結果に非同期で処理を適用するためです(以下の例では、ファイルの名前だけが出力されます)。

メインメソッドが終了しないようにスリープメソッドを使用しないようにするにはどうすればよいですか?チャンネルに頭を巻き付けるのに問題があります(結果を同期するにはそれが必要だと思います)。

package main

import (
    "fmt"
    "io/ioutil"
    "path"
    "path/filepath"
    "os"
    "runtime"
    "time"
)

func eachFile(extension string, callback func(file string)) {
    exeDir := filepath.Dir(os.Args[0])
    files, _ := ioutil.ReadDir(exeDir)
    for _, f := range files {
            fileName := f.Name()
            if extension == path.Ext(fileName) {
                go callback(fileName)
            }
    }
}


func main() {
    maxProcs := runtime.NumCPU()
    runtime.GOMAXPROCS(maxProcs)

    eachFile(".xml", func(fileName string) {
                // Custom logic goes in here
                fmt.Println(fileName)
            })

    // This is what i want to get rid of
    time.Sleep(100 * time.Millisecond)
}

回答:


173

sync.WaitGroupを使用できます。リンクされた例を引用すると:

package main

import (
        "net/http"
        "sync"
)

func main() {
        var wg sync.WaitGroup
        var urls = []string{
                "http://www.golang.org/",
                "http://www.google.com/",
                "http://www.somestupidname.com/",
        }
        for _, url := range urls {
                // Increment the WaitGroup counter.
                wg.Add(1)
                // Launch a goroutine to fetch the URL.
                go func(url string) {
                        // Decrement the counter when the goroutine completes.
                        defer wg.Done()
                        // Fetch the URL.
                        http.Get(url)
                }(url)
        }
        // Wait for all HTTP fetches to complete.
        wg.Wait()
}

11
goルーチンの外でwg.Add(1)を実行する必要がある理由は何ですか?据え置きwg.Done()の直前にそれを実行できますか?
2014年

18
sat、はい、理由があります、それはsync.WaitGroup.Addドキュメントに記載されています: Note that calls with positive delta must happen before the call to Wait, or else Wait may wait for too small a group. Typically this means the calls to Add should execute before the statement creating the goroutine or other event to be waited for. See the WaitGroup example.
wobmene

15
このコードを変更すると、長いデバッグセッションが発生しました。これは、私のルーチンが名前付き関数であり、WaitGroupに値として渡すと、コピーされてwg.Done()が無効になるためです。これはポインター&wgを渡すことで修正できますが、そのようなエラーを防ぐより良い方法は、WaitGroup変数を最初にポインターとして宣言することです:wg := new(sync.WaitGroup)ではなくvar wg sync.WaitGroup
ロバートジャックウィル

wg.Add(len(urls))行のすぐ上に書くことは有効だと思いfor _, url := range urlsますが、追加は1回だけ使用するのでより良いと思います。
ビクター

@RobertJackWill:注意してください!ちなみに、これはドキュメントで説明されています。「最初に使用した後は、WaitGroupをコピーしないでください。非常に悪いGoにはこれを強制する方法がありません。しかし、実際には、go vetこのケースが検出され、 "func pass lock by value :sync.WaitGroupは」sync.noCopyが含まれています。
ブレントBradburn

56

WaitGroupsは間違いなくこれを行うための標準的な方法です。ただし、完全を期すために、WaitGroupsが導入される前に一般的に使用されていたソリューションを次に示します。基本的な考え方は、チャネルを使用して「完了」と言うことであり、生成された各ルーチンがその完了を報告するまでメインのゴルーチンを待機させることです。

func main() {
    c := make(chan struct{}) // We don't need any data to be passed, so use an empty struct
    for i := 0; i < 100; i++ {
        go func() {
            doSomething()
            c <- struct{}{} // signal that the routine has completed
        }()
    }

    // Since we spawned 100 routines, receive 100 messages.
    for i := 0; i < 100; i++ {
        <- c
    }
}

9
プレーンチャネルのソリューションを見るのはいいことです。追加のボーナス:doSomething()結果が返された場合、それをチャネルに配置し、2番目のforループで結果を収集して処理できます(準備ができたらすぐに)
andras

4
開始したいゴルチンの量がすでにわかっている場合にのみ機能します。ある種のhtmlクローラーを作成し、ページ上のすべてのリンクに対して再帰的な方法でGorutinesを開始する場合はどうでしょうか。
shinydev

何とかしてこれを追跡する必要があります。WaitGroupsを使用すると、新しいゴルーチンをスポーンするたびに最初に実行できるwg.Add(1)ため、追跡が簡単になるため、少し簡単です。チャネルを使用すると、多少難しくなります。
joshlf 2016

すべてのgoルーチンがアクセスしようとするため、cはブロックされ、バッファ
リングされません

「ブロック」する場合、プログラムがデッドロックすることを意味しますが、それは正しくありません。自分で実行してみてください。その理由は、書き込みを行う唯一のゴルーチンは、cから読み取るメインのゴルーチンとは異なるためですc。したがって、メインゴルーチンは常にチャネルから値を読み取ることができます。これは、ゴルーチンの1つがチャネルに値を書き込むことができるときに発生します。このコードでゴルーチンが生成されず、すべてが単一のゴルーチンで実行された場合は、デッドロックが発生することは間違いありません。
joshlf 2018

8

sync.WaitGroupがここで役立ちます。

package main

import (
    "fmt"
    "sync"
    "time"
)


func wait(seconds int, wg * sync.WaitGroup) {
    defer wg.Done()

    time.Sleep(time.Duration(seconds) * time.Second)
    fmt.Println("Slept ", seconds, " seconds ..")
}


func main() {
    var wg sync.WaitGroup

    for i := 0; i <= 5; i++ {
        wg.Add(1)   
        go wait(i, &wg)
    }
    wg.Wait()
}

1

けれどもsync.waitGroup(WG)が前方に標準的な方法ですが、それはあなたがあなたの少なくともいくつか行う必要がないwg.Addあなたの前に呼び出しをwg.Wait完了するために、すべてのために。これは、再帰呼び出しの数が事前にわからず、wg.Add呼び出しを駆動するデータを取得するのに時間がかかるWebクローラーのような単純なものでは実行できない場合があります。結局、子ページの最初のバッチのサイズを知る前に、最初のページをロードして解析する必要があります。

私はチャネルを使用waitGroupしてソリューションを作成しましたが、私のソリューションではツアーのツアー-Webクローラー演習を避けました。1つ以上のgo-routineが開始されるたびに、その番号をchildrenチャネルに送信します。goルーチンが完了する直前1に、doneチャンネルにを送信します。子供の合計がdoneの合計と等しいとき、私たちは完了です。

私が残している唯一の懸念は、resultsチャネルのハードコーディングされたサイズですが、それは(現在の)Goの制限です。


// recursionController is a data structure with three channels to control our Crawl recursion.
// Tried to use sync.waitGroup in a previous version, but I was unhappy with the mandatory sleep.
// The idea is to have three channels, counting the outstanding calls (children), completed calls 
// (done) and results (results).  Once outstanding calls == completed calls we are done (if you are
// sufficiently careful to signal any new children before closing your current one, as you may be the last one).
//
type recursionController struct {
    results  chan string
    children chan int
    done     chan int
}

// instead of instantiating one instance, as we did above, use a more idiomatic Go solution
func NewRecursionController() recursionController {
    // we buffer results to 1000, so we cannot crawl more pages than that.  
    return recursionController{make(chan string, 1000), make(chan int), make(chan int)}
}

// recursionController.Add: convenience function to add children to controller (similar to waitGroup)
func (rc recursionController) Add(children int) {
    rc.children <- children
}

// recursionController.Done: convenience function to remove a child from controller (similar to waitGroup)
func (rc recursionController) Done() {
    rc.done <- 1
}

// recursionController.Wait will wait until all children are done
func (rc recursionController) Wait() {
    fmt.Println("Controller waiting...")
    var children, done int
    for {
        select {
        case childrenDelta := <-rc.children:
            children += childrenDelta
            // fmt.Printf("children found %v total %v\n", childrenDelta, children)
        case <-rc.done:
            done += 1
            // fmt.Println("done found", done)
        default:
            if done > 0 && children == done {
                fmt.Printf("Controller exiting, done = %v, children =  %v\n", done, children)
                close(rc.results)
                return
            }
        }
    }
}

ソリューションの完全なソースコード


1

以下は、WaitGroupを使用するソリューションです。

まず、2つのユーティリティメソッドを定義します。

package util

import (
    "sync"
)

var allNodesWaitGroup sync.WaitGroup

func GoNode(f func()) {
    allNodesWaitGroup.Add(1)
    go func() {
        defer allNodesWaitGroup.Done()
        f()
    }()
}

func WaitForAllNodes() {
    allNodesWaitGroup.Wait()
}

次に、以下の呼び出しを置き換えますcallback

go callback(fileName)

ユーティリティ関数を呼び出すと:

util.GoNode(func() { callback(fileName) })

最後のステップとしてmain、の代わりにの最後にこの行を追加しますsleep。これにより、プログラムが停止する前に、メインスレッドがすべてのルーチンの終了を確実に待機します。

func main() {
  // ...
  util.WaitForAllNodes()
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.