web-dev-qa-db-fra.com

terminaison goroutine idiomatique et gestion des erreurs

J'ai un cas d'utilisation simple d'accès simultané en cours, et cela me rend fou, je ne peux pas trouver une solution élégante. Toute aide serait appréciée.

Je veux écrire une méthode fetchAll qui interroge un nombre non spécifié de ressources à partir de serveurs distants en parallèle. Si l'un des récupérations échoue, je veux renvoyer cette première erreur immédiatement.

Ma mise en œuvre naïve initiale, fuit des goroutines:

package main

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

func fetchAll() error {
  wg := sync.WaitGroup{}
  errs := make(chan error)
  leaks := make(map[int]struct{})
  defer fmt.Println("these goroutines leaked:", leaks)

  // run all the http requests in parallel
  for i := 0; i < 4; i++ {
    leaks[i] = struct{}{}
    wg.Add(1)
    go func(i int) {
      defer wg.Done()
      defer delete(leaks, i)

      // pretend this does an http request and returns an error
      time.Sleep(time.Duration(Rand.Intn(100)) * time.Millisecond)
      errs <- fmt.Errorf("goroutine %d's error returned", i)
    }(i)
  }

  // wait until all the fetches are done and close the error
  // channel so the loop below terminates
  go func() {
    wg.Wait()
    close(errs)
  }()

  // return the first error
  for err := range errs {
    if err != nil {
      return err
    }
  }

  return nil
}

func main() {
  fmt.Println(fetchAll())
}

Aire de jeux: https://play.golang.org/p/Be93J514R5

Je sais en lisant https://blog.golang.org/pipelines que je peux créer un canal de signal pour nettoyer les autres threads. Alternativement, je pourrais probablement utiliser context pour l'accomplir. Mais il semble qu'un cas d'utilisation aussi simple devrait avoir une solution plus simple qui me manque.

15
gerad

Tous, sauf un de vos goroutines, ont une fuite, car ils attendent toujours d'être envoyés au canal des erreurs - vous ne terminez jamais la plage avant qui le vide. Vous divulguez également le goroutine qui doit fermer le canal des erreurs, car le groupe d'attente n'est jamais terminé.

(De plus, comme l'a souligné Andy, la suppression de la carte n'est pas adaptée aux threads, ce qui nécessite une protection contre un mutex.)

Cependant, je ne pense pas que les cartes, les mutex, les groupes d'attente, les contextes, etc. soient même nécessaires ici. Je réécrirais le tout pour utiliser simplement les opérations de canal de base, quelque chose comme ceci:

package main

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

func fetchAll() error {
    var N = 4
    quit := make(chan bool)
    errc := make(chan error)
    done := make(chan error)
    for i := 0; i < N; i++ {
        go func(i int) {
            // dummy fetch
            time.Sleep(time.Duration(Rand.Intn(100)) * time.Millisecond)
            err := error(nil)
            if Rand.Intn(2) == 0 {
                err = fmt.Errorf("goroutine %d's error returned", i)
            }
            ch := done // we'll send to done if nil error and to errc otherwise
            if err != nil {
                ch = errc
            }
            select {
            case ch <- err:
                return
            case <-quit:
                return
            }
        }(i)
    }
    count := 0
    for {
        select {
        case err := <-errc:
            close(quit)
            return err
        case <-done:
            count++
            if count == N {
                return nil // got all N signals, so there was no error
            }
        }
    }
}

func main() {
    Rand.Seed(time.Now().UnixNano())
    fmt.Println(fetchAll())
}

Lien aire de jeux: https://play.golang.org/p/mxGhSYYkOb

EDIT: Il y a effectivement eu une erreur stupide, merci de l'avoir signalé. J'ai corrigé le code ci-dessus (je pense ...). J'ai également ajouté un peu de hasard pour ajouter du réalisme ™.

Aussi, je voudrais souligner qu'il existe vraiment plusieurs façons d'aborder ce problème, et ma solution n'est qu'une façon. En fin de compte, cela se résume à un goût personnel, mais en général, vous voulez tendre vers un code "idiomatique" - et vers un style qui vous semble naturel et facile à comprendre.

15
LemurFromTheId

L'utilisation de Error Group rend cela encore plus simple. Cela attend automatiquement que toutes les routines Go fournies se terminent avec succès, ou annule toutes celles restantes dans le cas où une routine renvoie une erreur (auquel cas cette erreur est la seule bulle remontant à l'appelant).

package main

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

        "golang.org/x/sync/errgroup"
)

func fetchAll(ctx context.Context) error {
        errs, ctx := errgroup.WithContext(ctx)

        // run all the http requests in parallel
        for i := 0; i < 4; i++ {
                errs.Go(func() error {
                        // pretend this does an http request and returns an error                                                  
                        time.Sleep(time.Duration(Rand.Intn(100)) * time.Millisecond)                                               
                        return fmt.Errorf("goroutine %d's error returned", i)                                                      
                })
        }

        // Wait for completion and return the first error (if any)                                                                 
        return errs.Wait()
}

func main() {
        fmt.Println(fetchAll(context.Background()))
}
20
joth

Tant que chaque goroutine se termine, vous ne fuierez rien. Vous devez créer le canal d'erreur tel que mis en mémoire tampon avec une taille de tampon égale au nombre de goroutines afin que les opérations d'envoi sur le canal ne se bloquent pas. Chaque goroutine devrait toujours envoyer quelque chose sur le canal une fois terminé, qu'il réussisse ou échoue. La boucle en bas peut alors simplement répéter le nombre de goroutines et retourner si elle obtient une erreur non nulle. Vous n'avez pas besoin du WaitGroup ou de l'autre goroutine qui ferme le canal.

Je pense que la raison pour laquelle il semble que des goroutines fuient est que vous revenez lorsque vous obtenez la première erreur, donc certains d'entre eux fonctionnent toujours.

Au fait, les cartes ne sont pas sûres pour les goroutins. Si vous partagez une carte entre des goroutins et que certains d'entre eux apportent des modifications à la carte, vous devez la protéger avec un mutex.

2
Andy Schweig

Voici un exemple plus complet utilisant errgroup suggéré par joth . Il montre que le traitement des données a réussi et se terminera à la première erreur.

https://play.golang.org/p/rU1v-Mp2ijo

package main

import (
    "context"
    "fmt"
    "golang.org/x/sync/errgroup"
    "math/Rand"
    "time"
)

func fetchAll() error {
    g, ctx := errgroup.WithContext(context.Background())
    results := make(chan int)
    for i := 0; i < 4; i++ {
        current := i
        g.Go(func() error {
            // Simulate delay with random errors.
            time.Sleep(time.Duration(Rand.Intn(100)) * time.Millisecond)
            if Rand.Intn(2) == 0 {
                return fmt.Errorf("goroutine %d's error returned", current)
            }
            // Pass processed data to channel, or receive a context completion.
            select {
            case results <- current:
                return nil
            // Close out if another error occurs.
            case <-ctx.Done():
                return ctx.Err()
            }
        })
    }

    // Elegant way to close out the channel when the first error occurs or
    // when processing is successful.
    go func() {
        g.Wait()
        close(results)
    }()

    for result := range results {
        fmt.Println("processed", result)
    }

    // Wait for all fetches to complete.
    return g.Wait()
}

func main() {
    fmt.Println(fetchAll())
}

2
syvex