web-dev-qa-db-fra.com

Quel est l'avantage de sync.WaitGroup sur les canaux?

Je travaille sur une bibliothèque Go simultanée et je suis tombé sur deux modèles de synchronisation distincts entre goroutines dont les résultats sont similaires:

Utilisation de Waitgroup

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

Utiliser un canal

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
}

On m'a informé que sync.WaitGroup est légèrement plus performant et je l'ai vu utilisé couramment. Cependant, je trouve les canaux plus idiomatiques. Quel est le véritable avantage d'utiliser sync.WaitGroup sur les canaux et/ou quelle peut être la situation quand il est meilleur?

19
Pie 'Oh' Pah

Indépendamment de l'exactitude de votre deuxième exemple (comme expliqué dans les commentaires, vous ne faites pas ce que vous pensez, mais vous pouvez le réparer facilement), j'ai tendance à penser que le premier exemple est plus facile à comprendre.

Maintenant, je ne dirais même pas que les canaux sont plus idiomatiques. Les chaînes étant une caractéristique de la langue de Go, cela ne signifie pas qu’il soit idiomatique de les utiliser autant que possible. Ce qui est idiomatique dans Go, c’est d’utiliser la solution la plus simple et la plus facile à comprendre: ici, la variable WaitGroup transmet à la fois le sens (votre fonction principale est Waiting pour les travailleurs à effectuer) et le mécanicien (les travailleurs notifient quand ils sont Done).

Sauf si vous êtes dans un cas très spécifique, je ne recommande pas d'utiliser la solution de canal ici.

25
Elwinar

Si vous êtes particulièrement attentif à utiliser uniquement des canaux, cela doit être fait différemment (si nous utilisons votre exemple, comme le fait @Not_a_Golfer, cela produira des résultats incorrects). 

Une solution consiste à créer un canal de type int. Dans le processus de travail, envoyez un numéro chaque fois qu'il termine le travail (il peut également s'agir de l'identifiant de travail unique, si vous le souhaitez, vous pouvez le suivre dans le destinataire).

Dans la routine principale du récepteur (qui connaîtra le nombre exact de travaux soumis) - effectuez une boucle de distance sur un canal, comptez jusqu'à ce que le nombre de travaux soumis ne soit pas terminé et sortez de la boucle lorsque tous les travaux sont terminés. C'est un bon moyen si vous souhaitez suivre chacune des tâches terminées (et éventuellement faire quelque chose si nécessaire).

Voici le code pour votre référence. La décrémentation de totalJobsLeft sera sans danger car elle ne se fera que dans la boucle de la plage du canal! 

//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

Cela dépend du cas d'utilisation. Si vous répartissez des travaux uniques à exécuter en parallèle sans avoir besoin de connaître les résultats de chaque travail, vous pouvez utiliser une variable WaitGroup. Mais si vous devez collecter les résultats des goroutines, vous devez utiliser un canal.

Puisqu'une chaîne fonctionne dans les deux sens, j'utilise presque toujours une chaîne.

Sur une autre note, comme indiqué dans le commentaire, votre exemple de chaîne n'est pas implémenté correctement. Vous auriez besoin d'un canal séparé pour indiquer qu'il n'y a plus de tâches à faire (un exemple est ici ). Dans votre cas, puisque vous connaissez le nombre de mots à l'avance, vous pouvez utiliser un seul canal en mémoire tampon et recevoir un nombre fixe de fois pour éviter de déclarer un canal proche.

1
Wu-Man

Nous vous suggérons également d’utiliser waitgroup mais vous souhaitez tout de même le faire avec channel, puis je mentionne ci-dessous une utilisation simple de 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

J'utilise souvent des canaux pour collecter des messages d'erreur de goroutines susceptibles de générer une erreur. Voici un exemple simple:

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