web-dev-qa-db-fra.com

Ayez toujours x nombre de goroutines en cours d'exécution à tout moment

Je vois beaucoup de tutoriels et d'exemples sur la façon de faire aller Go jusqu'à ce que x nombre de goroutines se terminent, mais ce que j'essaie de faire est de m'assurer qu'il y a toujours x nombre en cours d'exécution, donc un nouveau goroutine est lancé dès que l'on se termine .

Plus précisément, j'ai quelques centaines de milliers de "choses à faire" qui traitent des trucs qui sortent de MySQL. Donc ça marche comme ça:

db, err := sql.Open("mysql", connection_string)
checkErr(err)
defer db.Close()

rows,err := db.Query(`SELECT id FROM table`)
checkErr(err)
defer rows.Close()

var id uint
for rows.Next() {
    err := rows.Scan(&id)
    checkErr(err)
    go processTheThing(id)
    }
checkErr(err)
rows.Close()

Actuellement, cela lancera plusieurs centaines de milliers de threads de processTheThing(). Ce dont j'ai besoin, c'est qu'un maximum de x (nous l'appellerons 20) goroutines sont lancés. Il commence donc par lancer 20 pour les 20 premières lignes, et à partir de là, il lancera un nouveau goroutine pour le prochain identifiant au moment où l'un des goroutines actuels est terminé. Donc à tout moment, il y en a toujours 20 en cours d'exécution.

Je suis sûr que c'est assez simple/standard, mais je n'arrive pas à trouver une bonne explication sur l'un des tutoriels ou des exemples ou comment cela est fait.

30
Alasdair

Merci à tous de m'avoir aidé. Cependant, je ne pense pas que quiconque ait vraiment fourni quelque chose à la fois efficace et simple/compréhensible, même si vous m'avez tous aidé à comprendre la technique.

Ce que j'ai fait à la fin, c'est que je pense que c'est beaucoup plus compréhensible et pratique comme réponse à ma question spécifique, donc je vais le poster ici au cas où quelqu'un d'autre aurait la même question.

D'une manière ou d'une autre, cela a fini par ressembler beaucoup à ce que OneOfOne a publié, ce qui est génial parce que maintenant je comprends cela. Mais le code de OneOfOne que j'ai trouvé très difficile à comprendre au début en raison du passage des fonctions aux fonctions, il était assez déroutant de comprendre quel bit était pour quoi. Je pense que cette façon a beaucoup plus de sens:

package main

import (
"fmt"
"sync"
)

const xthreads = 5 // Total number of threads to use, excluding the main() thread

func doSomething(a int) {
    fmt.Println("My job is",a)
    return
}

func main() {
    var ch = make(chan int, 50) // This number 50 can be anything as long as it's larger than xthreads
    var wg sync.WaitGroup

    // This starts xthreads number of goroutines that wait for something to do
    wg.Add(xthreads)
    for i:=0; i<xthreads; i++ {
        go func() {
            for {
                a, ok := <-ch
                if !ok { // if there is nothing to do and the channel has been closed then end the goroutine
                    wg.Done()
                    return
                }
                doSomething(a) // do the thing
            }
        }()
    }

    // Now the jobs can be added to the channel, which is used as a queue
    for i:=0; i<50; i++ {
        ch <- i // add i to the queue
    }

    close(ch) // This tells the goroutines there's nothing else to do
    wg.Wait() // Wait for the threads to finish
}
15
Alasdair

Vous pouvez trouver Go Concurrency Patterns article intéressant, en particulier section Parallélisme borné , il explique le modèle exact dont vous avez besoin.

Vous pouvez utiliser canal de structures vides comme garde limite pour contrôler le nombre de goroutines de travailleurs simultanés :

package main

import "fmt"

func main() {
    maxGoroutines := 10
    guard := make(chan struct{}, maxGoroutines)

    for i := 0; i < 30; i++ {
        guard <- struct{}{} // would block if guard channel is already filled
        go func(n int) {
            worker(n)
            <-guard
        }(i)
    }
}

func worker(i int) { fmt.Println("doing work on", i) }
39
artyom
  1. Créez un canal pour transmettre des données aux goroutines.
  2. Démarrez 20 goroutines qui traitent les données du canal en boucle.
  3. Envoyez les données au canal au lieu de démarrer un nouveau goroutine.
14
Grzegorz Żur

Ici, je pense que quelque chose de simple comme celui-ci fonctionnera:

package main

import "fmt"

const MAX = 20

func main() {
    sem := make(chan int, MAX)
    for {
        sem <- 1 // will block if there is MAX ints in sem
        go func() {
            fmt.Println("hello again, world")
            <-sem // removes an int from sem, allowing another to proceed
        }()
    }
}
12
Emil Davtyan

Grzegorz Żur's answer est le moyen le plus efficace de le faire, mais pour un nouveau venu, il pourrait être difficile de l'implémenter sans lire le code, alors voici une implémentation très simple:

type idProcessor func(id uint)

func SpawnStuff(limit uint, proc idProcessor) chan<- uint {
    ch := make(chan uint)
    for i := uint(0); i < limit; i++ {
        go func() {
            for {
                id, ok := <-ch
                if !ok {
                    return
                }
                proc(id)
            }
        }()
    }
    return ch
}

func main() {
    runtime.GOMAXPROCS(4)
    var wg sync.WaitGroup //this is just for the demo, otherwise main will return
    fn := func(id uint) {
        fmt.Println(id)
        wg.Done()
    }
    wg.Add(1000)
    ch := SpawnStuff(10, fn)
    for i := uint(0); i < 1000; i++ {
        ch <- i
    }
    close(ch) //should do this to make all the goroutines exit gracefully
    wg.Wait()
}

playground

10
OneOfOne

Il s'agit d'un simple problème producteur-consommateur , qui dans Go peut être facilement résolu en utilisant des canaux pour tamponner les paquets.

Pour faire simple: créez une chaîne qui accepte vos identifiants. Exécutez un certain nombre de routines qui liront le canal dans une boucle, puis traiter l'ID. Exécutez ensuite votre boucle qui fournira des identifiants au canal.

Exemple:

func producer() {
    var buffer = make(chan uint)

    for i := 0; i < 20; i++ {
        go consumer(buffer)
    }

    for _, id :=  range IDs {
        buffer <- id
    }
}

func consumer(buffer chan uint) {
    for {
        id := <- buffer
        // Do your things here
    }
}

À savoir:

  • Les canaux sans tampon bloquent: si l'élément écrit dans le canal n'est pas accepté, la routine alimentant l'élément se bloquera jusqu'à ce qu'il soit
  • Mon exemple manque de mécanisme de fermeture: il faut trouver un moyen de faire attendre le producteur que tous les consommateurs mettent fin à leur boucle avant de revenir. La façon la plus simple de le faire est d'utiliser un autre canal. Je te laisse réfléchir.
2
Elwinar