it-swarm.com.de

Was ist der Vorteil von sync.WaitGroup gegenüber Channels?

Ich arbeite an einer gleichzeitigen Go-Bibliothek und bin auf zwei unterschiedliche Synchronisationsmuster zwischen Goroutinen gestoßen, deren Ergebnisse ähnlich sind:

Waitgroup verwenden

var wg sync.WaitGroup
func main() {
        words := []string{ "foo", "bar", "baz" }

        for _, Word := range words {
                wg.Add(1)
                go func(Word string) {
                        time.Sleep(1 * time.Second)
                        defer wg.Done()
                        fmt.Println(Word)
                }(Word)
        }
        // do concurrent things here

        // blocks/waits for waitgroup
        wg.Wait()
}

Kanal verwenden

func main() {
        words = []string{ "foo", "bar", "baz" }
        done := make(chan bool)
        defer close(done)
        for _, Word := range words {
                go func(Word string) {
                        time.Sleep(1 * time.Second)
                        fmt.Println(Word)
                        done <- true
                }(Word)
        }

        // Do concurrent things here

        // This blocks and waits for signal from channel
        <-done
}

Mir wurde gesagt, dass sync.WaitGroup etwas performanter ist, und ich habe gesehen, dass es häufig verwendet wird. Ich finde jedoch Kanäle idiomatischer. Was ist der wirkliche Vorteil der Verwendung von sync.WaitGroup über Channels und/oder was könnte die Situation sein, wenn es besser ist?

19
Pie 'Oh' Pah

Unabhängig von der Richtigkeit Ihres zweiten Beispiels (wie in den Kommentaren erläutert, tun Sie nicht das, was Sie denken, aber es ist leicht zu beheben), neige ich zu der Auffassung, dass das erste Beispiel leichter zu verstehen ist.

Nun würde ich nicht einmal sagen, dass Kanäle idiomatischer sind. Kanäle, die ein Signaturmerkmal der Go-Sprache sind, sollten nicht bedeuten, dass sie nach Möglichkeit verwendet werden. In Go ist es idiomatisch, die einfachste und am einfachsten zu verstehende Lösung zu verwenden: Die WaitGroup vermittelt sowohl die Bedeutung (Ihre Hauptfunktion ist Waiting für die zu erledigenden Arbeiter) als auch der Mechaniker (die Arbeiter benachrichtigen, wenn sie Done sind).

Wenn Sie nicht in einem bestimmten Fall sind, empfehle ich nicht, die Kanallösung hier zu verwenden.

25
Elwinar

Wenn Sie bei der Verwendung von Kanälen besonders hartnäckig sind, müssen Sie dies anders tun (wenn Sie Ihr Beispiel verwenden, wie @Not_a_Golfer darauf hinweist, führt dies zu falschen Ergebnissen). 

Eine Möglichkeit besteht darin, einen Kanal vom Typ int zu erstellen. Senden Sie im Worker-Prozess jedes Mal, wenn der Job abgeschlossen ist, eine Nummer (dies kann auch die eindeutige Job-ID sein, wenn Sie möchten, können Sie dies im Empfänger nachverfolgen).

Gehen Sie in der Hauptroutine des Empfängers (die die genaue Anzahl der gesendeten Jobs kennt) durch: Führen Sie eine Bereichsschleife über einen Kanal aus, zählen Sie, bis die Anzahl der gesendeten Jobs nicht abgeschlossen ist, und brechen Sie die Schleife ab, wenn alle Jobs abgeschlossen sind. Dies ist eine gute Methode, wenn Sie den Abschluss jedes Jobs nachverfolgen möchten (und ggf. etwas tun).

Hier ist der Code für Ihre Referenz. Wenn Sie totalJobsLeft dekrementieren, ist dies sicher, da dies nur in der Range-Schleife des Kanals geschieht. 

//This is just an illustration of how to sync completion of multiple jobs using a channel
//A better way many a times might be to use wait groups

package main

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

func main() {

    comChannel := make(chan int)
    words := []string{"foo", "bar", "baz"}

    totalJobsLeft := len(words)

    //We know how many jobs are being sent

    for j, Word := range words {
        jobId := j + 1
        go func(Word string, jobId int) {

            fmt.Println("Job ID:", jobId, "Word:", Word)
            //Do some work here, maybe call functions that you need
            //For emulating this - Sleep for a random time upto 5 seconds
            randInt := Rand.Intn(5)
            //fmt.Println("Got random number", randInt)
            time.Sleep(time.Duration(randInt) * time.Second)
            comChannel <- jobId
        }(Word, jobId)
    }

    for j := range comChannel {
        fmt.Println("Got job ID", j)
        totalJobsLeft--
        fmt.Println("Total jobs left", totalJobsLeft)
        if totalJobsLeft == 0 {
            break
        }
    }
    fmt.Println("Closing communication channel. All jobs completed!")
    close(comChannel)

}
2
Ravi

Das hängt vom Anwendungsfall ab. Wenn Sie einmalige Jobs zur Ausführung ausführen, ohne die Ergebnisse jedes Jobs kennen zu müssen, können Sie eine WaitGroup verwenden. Wenn Sie jedoch die Ergebnisse der Goroutinen sammeln müssen, sollten Sie einen Kanal verwenden.

Da ein Kanal in beide Richtungen funktioniert, verwende ich fast immer einen Kanal.

Wie bereits erwähnt, ist Ihr Kanalbeispiel nicht korrekt implementiert. Sie benötigen einen separaten Kanal, um anzuzeigen, dass keine weiteren Jobs mehr zu erledigen sind (ein Beispiel ist hier ). In Ihrem Fall können Sie, da Sie die Anzahl der Wörter im Voraus kennen, nur einen gepufferten Kanal verwenden und eine festgelegte Anzahl von Malen empfangen, um zu vermeiden, dass ein geschlossener Kanal deklariert wird.

1
Wu-Man

Schlage auch vor, waitgroup zu verwenden, aber du willst es trotzdem mit Channel machen, dann erwähne ich eine einfache Verwendung von Channel

package main

import (
    "fmt"
    "time"
)

func main() {
    c := make(chan string)
    words := []string{"foo", "bar", "baz"}

    go printWordrs(words, c)

    for j := range c {
        fmt.Println(j)
    }
}


func printWordrs(words []string, c chan string) {
    defer close(c)
    for _, Word := range words {
        time.Sleep(1 * time.Second)
        c <- Word
    }   
}
0
Deepak Gupta

Ich verwende oft Kanäle, um Fehlermeldungen von Goroutines zu sammeln, die einen Fehler erzeugen könnten. Hier ist ein einfaches Beispiel:

func couldGoWrong() (err error) {
    errorChannel := make(chan error, 3)

    // start a go routine
    go func() (err error) {
        defer func() { errorChannel <- err }()

        for c := 0; c < 10; c++ {
            _, err = fmt.Println(c)
            if err != nil {
                return
            }
        }

        return
    }()

    // start another go routine
    go func() (err error) {
        defer func() { errorChannel <- err }()

        for c := 10; c < 100; c++ {
            _, err = fmt.Println(c)
            if err != nil {
                return
            }
        }

        return
    }()

    // start yet another go routine
    go func() (err error) {
        defer func() { errorChannel <- err }()

        for c := 100; c < 1000; c++ {
            _, err = fmt.Println(c)
            if err != nil {
                return
            }
        }

        return
    }()

    // synchronize go routines and collect errors here
    for c := 0; c < cap(errorChannel); c++ {
        err = <-errorChannel
        if err != nil {
            return
        }
    }

    return
}
0
Elmer