Esempio per sync.WaitGroup corretto?


108

Questo esempio di utilizzo è sync.WaitGroupcorretto? Fornisce il risultato atteso, ma non sono sicuro di wg.Add(4)e della posizione di wg.Done(). Ha senso aggiungere le quattro goroutine contemporaneamente wg.Add()?

http://play.golang.org/p/ecvYHiie0P

package main

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

func dosomething(millisecs time.Duration, wg *sync.WaitGroup) {
    duration := millisecs * time.Millisecond
    time.Sleep(duration)
    fmt.Println("Function in background, duration:", duration)
    wg.Done()
}

func main() {
    var wg sync.WaitGroup
    wg.Add(4)
    go dosomething(200, &wg)
    go dosomething(400, &wg)
    go dosomething(150, &wg)
    go dosomething(600, &wg)

    wg.Wait()
    fmt.Println("Done")
}

Risultato (come previsto):

Function in background, duration: 150ms
Function in background, duration: 200ms
Function in background, duration: 400ms
Function in background, duration: 600ms
Done

1
E se dosomething () si blocca prima di poter eseguire wg.Done ()?
Mostowski Crollo

19
Mi rendo conto che questo è vecchio, ma per le persone future, consiglierei una defer wg.Done()chiamata iniziale all'inizio della funzione.
Brian

Risposte:


154

Sì, questo esempio è corretto. È importante che ciò wg.Add()avvenga prima godell'istruzione per evitare condizioni di gara. Anche quanto segue sarebbe corretto:

func main() {
    var wg sync.WaitGroup
    wg.Add(1)
    go dosomething(200, &wg)
    wg.Add(1)
    go dosomething(400, &wg)
    wg.Add(1)
    go dosomething(150, &wg)
    wg.Add(1)
    go dosomething(600, &wg)

    wg.Wait()
    fmt.Println("Done")
}

Tuttavia, è piuttosto inutile chiamare wg.Addpiù e più volte quando sai già quante volte verrà chiamato.


Waitgroupspanico se il contatore scende sotto lo zero. Il contatore parte da zero, ognuno Done()è un -1e ciascuno Add()dipende dal parametro. Quindi, per assicurarti che il contatore non scenda mai al di sotto ed evitare il panico, devi Add()essere sicuro che venga prima del Done().

In Go, tali garanzie sono date dal modello di memoria .

Il modello di memoria afferma che tutte le istruzioni in una singola goroutine sembrano essere eseguite nello stesso ordine in cui sono scritte. È possibile che non siano effettivamente in quell'ordine, ma il risultato sarà come se lo fosse. È anche garantito che una goroutine non viene eseguita fino a dopo l' goistruzione che la chiama . Poiché l' istruzione si Add()verifica prima godell'istruzione e l' goistruzione prima dell'istruzione Done(), sappiamo che si Add()verifica prima dell'istruzione Done().

Se si dovesse avere l' goistruzione prima di Add(), il programma potrebbe funzionare correttamente. Tuttavia, sarebbe una condizione di gara perché non sarebbe garantita.


9
Ho una domanda su questo: non sarebbe meglio defer wg.Done()così essere sicuri che venga chiamato indipendentemente dal percorso che prende la goroutine? Grazie.
Alessandro Santini

2
se si desidera semplicemente assicurarsi che la funzione non venga restituita prima che tutte le routine di go siano terminate, si preferisce sì defer. solo di solito il punto centrale di un gruppo di attesa è aspettare che tutto il lavoro sia finito per poi fare qualcosa con i risultati che stavi aspettando.
Zanven

1
Se non lo usi defere uno dei tuoi goroutine non riesce a chiamare wg.Done()... non ti Waitbloccherai semplicemente per sempre? Sembra che potrebbe facilmente introdurre un bug difficile da trovare nel tuo codice ...
Dan Esparza

29

Consiglierei di incorporare la wg.Add()chiamata nella doSomething()funzione stessa, in modo che se modifichi il numero di volte che viene chiamato, non devi regolare separatamente il parametro di aggiunta manualmente, il che potrebbe portare a un errore se ne aggiorni uno ma dimentichi di aggiornare il altro (in questo banale esempio che è improbabile, ma ancora, personalmente credo che sia una pratica migliore per il riutilizzo del codice).

Come sottolinea Stephen Weinberg nella sua risposta a questa domanda , devi incrementare il gruppo di attesa prima di generare il gofunc, ma puoi farlo facilmente avvolgendo lo spawn gofunc all'interno della doSomething()funzione stessa, in questo modo:

func dosomething(millisecs time.Duration, wg *sync.WaitGroup) {
    wg.Add(1)
    go func() {
        duration := millisecs * time.Millisecond
        time.Sleep(duration)
        fmt.Println("Function in background, duration:", duration)
        wg.Done()
    }()
}

Quindi puoi chiamarlo senza l' goinvocazione, ad esempio:

func main() {
    var wg sync.WaitGroup
    dosomething(200, &wg)
    dosomething(400, &wg)
    dosomething(150, &wg)
    dosomething(600, &wg)
    wg.Wait()
    fmt.Println("Done")
}

Come parco giochi: http://play.golang.org/p/WZcprjpHa_


21
  • piccolo miglioramento basato sulla risposta di Mroth
  • usare il differimento per Fatto è più sicuro
  func dosomething(millisecs time.Duration, wg *sync.WaitGroup) {
  wg.Add(1)
  go func() {
      defer wg.Done()
      duration := millisecs * time.Millisecond
      time.Sleep(duration)
      fmt.Println("Function in background, duration:", duration)
  }()
}

func main() {
  var wg sync.WaitGroup
  dosomething(200, &wg)
  dosomething(400, &wg)
  dosomething(150, &wg)
  dosomething(600, &wg)
  wg.Wait()
  fmt.Println("Done")
}
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.