Come aspettare che tutte le goroutine finiscano senza usare il tempo.


108

Questo codice seleziona tutti i file xml nella stessa cartella, come l'eseguibile richiamato e applica in modo asincrono l'elaborazione a ogni risultato nel metodo di callback (nell'esempio seguente, viene stampato solo il nome del file).

Come posso evitare di utilizzare il metodo sleep per impedire la chiusura del metodo principale? Ho problemi a capire i canali (presumo sia quello che serve, per sincronizzare i risultati), quindi qualsiasi aiuto è apprezzato!

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

Risposte:


173

Puoi usare sync.WaitGroup . Citando l'esempio collegato:

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
C'è qualche motivo per cui devi eseguire wg.Add (1) al di fuori della routine go? Possiamo farlo subito prima del defer wg.Done ()?
sabato

18
sat, sì, c'è un motivo, è descritto in sync.WaitGroup.Aggiungi documenti: 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
L'adattamento di questo codice mi ha causato una lunga sessione di debug perché la mia goroutine era una funzione denominata e passare in WaitGroup come valore lo copierà e renderà wg.Done () inefficace. Sebbene questo possa essere risolto passando un puntatore & wg, un modo migliore per prevenire tali errori è dichiarare la variabile WaitGroup come un puntatore in primo luogo: wg := new(sync.WaitGroup)invece di var wg sync.WaitGroup.
Robert Jack Will

Immagino sia valido scrivere wg.Add(len(urls))appena sopra la riga for _, url := range urls, credo sia meglio dato che usi Add solo una volta.
Victor

@RobertJackWill: buona nota! A proposito, questo è trattato nei documenti : "Un gruppo di attesa non deve essere copiato dopo il primo utilizzo. Peccato che Go non abbia modo di imporlo . In realtà, tuttavia, go vetrileva questo caso e avverte con" func passa il blocco per valore : sync.WaitGroup contiene sync.noCopy ".
Brent Bradburn,

56

I WaitGroup sono sicuramente il modo canonico per farlo. Per ragioni di completezza, però, ecco la soluzione comunemente utilizzata prima dell'introduzione di WaitGroups. L'idea di base è usare un canale per dire "Ho finito" e fare in modo che la goroutine principale attenda finché ogni routine generata non ha segnalato il suo completamento.

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
Bello vedere una soluzione con canali semplici. Un ulteriore vantaggio: se doSomething()restituisce un risultato, puoi metterlo sul canale e puoi raccogliere ed elaborare i risultati nel secondo ciclo for (non appena sono pronti)
andras

4
Funziona solo se conosci già la quantità di gorutine che vorresti iniziare. E se stai scrivendo una sorta di crawler html e avvii le gorutine in modo ricorsivo per ogni link sulla pagina?
shinydev

Dovrai tenerne traccia in qualche modo a prescindere. Con WaitGroups è un po 'più facile perché ogni volta che generi una nuova goroutine, puoi prima farlo wg.Add(1)e quindi ne terrà traccia. Con i canali sarebbe un po 'più difficile.
joshlf

c si bloccherà poiché tutte le routine di go proveranno ad accedervi, ed è senza buffer
Edwin Ikechukwu Okonkwo

Se per "blocco" intendi che il programma andrà in deadlock, non è vero. Puoi provare a eseguirlo da solo. Il motivo è che le uniche goroutine che scrivono csono diverse dalla goroutine principale, che legge da c. Pertanto, la goroutine principale è sempre disponibile per leggere un valore dal canale, cosa che accadrà quando una delle goroutine è disponibile per scrivere un valore sul canale. Hai ragione che se questo codice non generasse le goroutine ma invece eseguisse tutto in una singola goroutine, sarebbe un deadlock.
joshlf

8

sync.WaitGroup può aiutarti qui.

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

Sebbene sync.waitGroup(wg) sia il modo canonico in avanti, richiede che tu esegua almeno alcune delle tue wg.Addchiamate prima di essere wg.Waitcompletato. Ciò potrebbe non essere fattibile per cose semplici come un web crawler, in cui non si conosce in anticipo il numero di chiamate ricorsive e ci vuole un po 'per recuperare i dati che guidano le wg.Addchiamate. Dopo tutto, è necessario caricare e analizzare la prima pagina prima di conoscere la dimensione del primo batch di pagine figlie.

Ho scritto una soluzione utilizzando i canali, evitando waitGroupnella mia soluzione il Tour of Go - esercizio di web crawler . Ogni volta che vengono avviate una o più routine di go, si invia il numero al childrencanale. Ogni volta che una routine go sta per essere completata, invii un messaggio 1al donecanale. Quando la somma dei bambini è uguale alla somma di fatto, abbiamo finito.

La mia unica preoccupazione rimasta è la dimensione hardcoded del resultscanale, ma questa è una limitazione (attuale) di 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
            }
        }
    }
}

Codice sorgente completo per la soluzione


1

Ecco una soluzione che utilizza WaitGroup.

Innanzitutto, definisci 2 metodi di utilità:

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

Quindi, sostituire l'invocazione di callback:

go callback(fileName)

Con una chiamata alla tua funzione di utilità:

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

Ultimo passaggio, aggiungi questa riga alla fine del tuo main, invece del tuo sleep. Questo assicurerà che il thread principale stia aspettando che tutte le routine finiscano prima che il programma possa fermarsi.

func main() {
  // ...
  util.WaitForAllNodes()
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.