web-dev-qa-db-fra.com

Comment définiriez-vous un pool de goroutines à exécuter à la fois à Golang?

TL; TR: Veuillez simplement aller à la dernière partie et me dire comment vous pourriez résoudre ce problème.

J'ai commencé à utiliser Golang ce matin en provenance de Python. Je veux appeler plusieurs fois un exécutable de source fermée à partir de Go, avec un bit de concurrence, avec différents arguments de ligne de commande. Mon code résultant fonctionne très bien mais j'aimerais avoir votre avis afin de l'améliorer. Comme je suis à un stade précoce d'apprentissage, je vais également expliquer mon flux de travail.

Par souci de simplicité, supposons ici que ce "programme externe de source fermée" est zenity, un outil de ligne de commande Linux qui peut afficher des boîtes de message graphiques à partir de la ligne de commande.

Appel d'un fichier exécutable à partir de Go

Donc, dans Go, j'irais comme ceci:

package main
import "os/exec"
func main() {
    cmd := exec.Command("zenity", "--info", "--text='Hello World'")
    cmd.Run()
}

Cela devrait fonctionner parfaitement. Notez que .Run() est un équivalent fonctionnel à .Start() suivi de .Wait(). C'est génial, mais si je voulais exécuter ce programme une seule fois, tout le programme ne valait pas la peine. Faisons donc cela plusieurs fois.

Appeler un exécutable plusieurs fois

Maintenant que cela fonctionnait, je voudrais appeler mon programme plusieurs fois, avec des arguments de ligne de commande personnalisés (ici juste i pour des raisons de simplicité).

package main    
import (
    "os/exec"
    "strconv"
)

func main() {
    NumEl := 8 // Number of times the external program is called
    for i:=0; i<NumEl; i++ {
        cmd := exec.Command("zenity", "--info", "--text='Hello from iteration n." + strconv.Itoa(i) + "'")
        cmd.Run()
    }
}

D'accord, nous l'avons fait! Mais je ne vois toujours pas l'avantage de Go over Python… Ce morceau de code est en fait exécuté de façon sérielle. J'ai un processeur multicœur et j'aimerais en profiter. Ajoutons donc un peu de simultanéité avec les goroutines.

Goroutines, ou un moyen de rendre mon programme parallèle

a) Première tentative: ajoutez simplement "go" partout

Réécrivons notre code pour rendre les choses plus faciles à appeler et à réutiliser et ajoutons le fameux mot clé go:

package main
import (
    "os/exec"
    "strconv"
)

func main() {
    NumEl := 8 
    for i:=0; i<NumEl; i++ {
        go callProg(i)  // <--- There!
    }
}

func callProg(i int) {
    cmd := exec.Command("zenity", "--info", "--text='Hello from iteration n." + strconv.Itoa(i) + "'")
    cmd.Run()
}

Rien! Quel est le problème? Tous les goroutines sont exécutés en même temps. Je ne sais pas vraiment pourquoi zenity n'est pas exécuté mais AFAIK, le programme Go s'est arrêté avant que le programme externe zenity puisse même être initialisé. Cela a été confirmé par l'utilisation de time.Sleep: Attendre quelques secondes était suffisant pour permettre à l'instance 8 de zenity de se lancer. Je ne sais pas si cela peut être considéré comme un bug.

Pour aggraver les choses, le vrai programme que j'aimerais appeler prend du temps à s'exécuter. Si j'exécute 8 instances de ce programme en parallèle sur mon processeur à 4 cœurs, ça va perdre du temps à faire beaucoup de changement de contexte… Je ne sais pas comment les goroutines Go se comportent, mais exec.Commandva lancer zenity 8 fois dans 8 threads différents. Pour aggraver encore les choses, je veux exécuter ce programme plus de 100 000 fois. Faire tout cela à la fois dans des goroutins ne sera pas du tout efficace. Je voudrais quand même tirer parti de mon processeur à 4 cœurs!

b) Deuxième tentative: utiliser des pools de goroutines

Les ressources en ligne ont tendance à recommander l'utilisation de sync.WaitGroup Pour ce type de travail. Le problème avec cette approche est que vous travaillez essentiellement avec des lots de goroutines: si je crée WaitGroup de 4 membres, le programme Go attendra tous les 4 programmes externes pour terminer avant d'appeler un nouveau lot de 4 programmes. Ce n'est pas efficace: le CPU est gaspillé, encore une fois.

Certaines autres ressources ont recommandé l'utilisation d'un canal en mémoire tampon pour effectuer le travail:

package main
import (
    "os/exec"
    "strconv"
)

func main() {
    NumEl := 8               // Number of times the external program is called
    NumCore := 4             // Number of available cores
    c := make(chan bool, NumCore - 1) 
    for i:=0; i<NumEl; i++ {
        go callProg(i, c)
        c <- true            // At the NumCoreth iteration, c is blocking   
    }
}

func callProg(i int, c chan bool) {
    defer func () {<- c}()
    cmd := exec.Command("zenity", "--info", "--text='Hello from iteration n." + strconv.Itoa(i) + "'")
    cmd.Run()
}

Cela semble moche. Les chaînes n'étaient pas prévues à cet effet: j'exploite un effet secondaire. J'adore le concept de defer mais je déteste devoir déclarer une fonction (même un lambda) pour faire sortir une valeur du canal factice que j'ai créé. Oh, et bien sûr, utiliser un canal factice est, en soi, moche.

c) Troisième tentative: mourir quand tous les enfants sont morts

Maintenant, nous avons presque terminé. Je dois juste prendre en compte un autre effet secondaire: le programme Go se ferme avant que toutes les fenêtres pop-up zenity ne soient fermées. En effet, lorsque la boucle est finie (à la 8ème itération), rien n'empêche le programme de se terminer. Cette fois, sync.WaitGroup Sera utile.

package main
import (
    "os/exec"
    "strconv"
    "sync"
)

func main() {
    NumEl := 8               // Number of times the external program is called
    NumCore := 4             // Number of available cores
    c := make(chan bool, NumCore - 1) 
    wg := new(sync.WaitGroup)
    wg.Add(NumEl)            // Set the number of goroutines to (0 + NumEl)
    for i:=0; i<NumEl; i++ {
        go callProg(i, c, wg)
        c <- true            // At the NumCoreth iteration, c is blocking   
    }
    wg.Wait() // Wait for all the children to die
    close(c)
}

func callProg(i int, c chan bool, wg *sync.WaitGroup) {
    defer func () {
        <- c
        wg.Done() // Decrease the number of alive goroutines
    }()
    cmd := exec.Command("zenity", "--info", "--text='Hello from iteration n." + strconv.Itoa(i) + "'")
    cmd.Run()
}

Terminé.

Mes questions

  • Connaissez-vous un autre moyen approprié pour limiter le nombre de goroutines exécutées à la fois?

Je ne parle pas de fils; comment Go gère les goroutines en interne n'est pas pertinent. Je veux vraiment dire limiter le nombre de goroutines lancées en même temps: exec.Command Crée un nouveau thread à chaque appel, donc je devrais contrôler le nombre de fois qu'il est appelé.

  • Est-ce que ce code vous convient?
  • Savez-vous comment éviter l'utilisation d'un canal factice dans ce cas?

Je ne peux pas me convaincre que ces canaux factices sont la voie à suivre.

64
user1940040

Je ferais apparaître 4 goroutines ouvrières qui lisent les tâches à partir d'un canal commun. Les goroutines qui sont plus rapides que les autres (car elles sont programmées différemment ou obtiennent des tâches simples) recevront plus de tâches de ce canal que les autres. En plus de cela, j'utiliserais un sync.WaitGroup pour attendre que tous les travailleurs aient fini. La partie restante n'est que la création des tâches. Vous pouvez voir un exemple d'implémentation de cette approche ici:

package main

import (
    "os/exec"
    "strconv"
    "sync"
)

func main() {
    tasks := make(chan *exec.Cmd, 64)

    // spawn four worker goroutines
    var wg sync.WaitGroup
    for i := 0; i < 4; i++ {
        wg.Add(1)
        go func() {
            for cmd := range tasks {
                cmd.Run()
            }
            wg.Done()
        }()
    }

    // generate some tasks
    for i := 0; i < 10; i++ {
        tasks <- exec.Command("zenity", "--info", "--text='Hello from iteration n."+strconv.Itoa(i)+"'")
    }
    close(tasks)

    // wait for the workers to finish
    wg.Wait()
}

Il existe probablement d'autres approches possibles, mais je pense que c'est une solution très propre et facile à comprendre.

87
tux21b

Une approche simple de la limitation (exécutez f() N fois mais maximum maxConcurrency simultanément), juste un schéma:

package main

import (
        "sync"
)

const maxConcurrency = 4 // for example

var throttle = make(chan int, maxConcurrency)

func main() {
        const N = 100 // for example
        var wg sync.WaitGroup
        for i := 0; i < N; i++ {
                throttle <- 1 // whatever number
                wg.Add(1)
                go f(i, &wg, throttle)
        }
        wg.Wait()
}

func f(i int, wg *sync.WaitGroup, throttle chan int) {
        defer wg.Done()
        // whatever processing
        println(i)
        <-throttle
}

Aire de jeux

Je n'appellerais probablement pas le canal throttle "factice". À mon humble avis, c'est une manière élégante (ce n'est pas mon invention bien sûr), comment limiter la concurrence.

BTW: Veuillez noter que vous ignorez l'erreur renvoyée par cmd.Run().

34
zzzz

essayez ceci: https://github.com/korovkin/limiter

 limiter := NewConcurrencyLimiter(10)
 limiter.Execute(func() {
        zenity(...) 
 })
 limiter.Wait()
1
korovkin