web-dev-qa-db-fra.com

Concurrence de Golang: comment ajouter à la même tranche de goroutines différentes

J'ai plusieurs goroutines concurrentes qui veulent ajouter un (pointeur à une) structure à la même tranche . Comment écrivez-vous cela dans Go pour le rendre protégé contre la concurrence?

Ce serait mon code concurrentiel non sécurisé, en utilisant un groupe d'attente:

var wg sync.WaitGroup
MySlice = make([]*MyStruct)
for _, param := range params {
    wg.Add(1)
    go func(param string) {
        defer wg.Done()
        OneOfMyStructs := getMyStruct(param)
        MySlice = append(MySlice, &OneOfMyStructs)
    }(param)
}
wg.Wait()

Je suppose que vous auriez besoin d'utiliser des canaux Go pour la sécurité d'accès simultané. Quelqu'un peut-il contribuer avec un exemple?

15
Daniele B

Il n'y a rien de mal à garder la MySlice = append(MySlice, &OneOfMyStructs) avec un sync.Mutex. Mais bien sûr, vous pouvez avoir un canal de résultat avec une taille de tampon len(params), toutes les goroutines envoient leurs réponses et une fois votre travail terminé, vous récupérez ce canal de résultat.

Si votre params a une taille fixe:

MySlice = make([]*MyStruct, len(params))
for i, param := range params {
    wg.Add(1)
    go func(i int, param string) {
         defer wg.Done()
         OneOfMyStructs := getMyStruct(param)
         MySlice[i] = &OneOfMyStructs
     }(i, param)
}

Comme tous les goroutines écrivent dans une mémoire différente, cela n’est pas racé.

19
Volker

La réponse publiée par @jimt n’est pas tout à fait correcte, car elle manque la dernière valeur envoyée dans le canal et la dernière defer wg.Done() n’est jamais appelée. L'extrait ci-dessous contient les corrections.

https://play.golang.org/p/7N4sxD-Bai

package main

import "fmt"
import "sync"

type T int

func main() {
    var slice []T
    var wg sync.WaitGroup

    queue := make(chan T, 1)

    // Create our data and send it into the queue.
    wg.Add(100)
    for i := 0; i < 100; i++ {
        go func(i int) {
            // defer wg.Done()  <- will result in the last int to be missed in the receiving channel
            queue <- T(i)
        }(i)
    }

    go func() {
        // defer wg.Done() <- Never gets called since the 100 `Done()` calls are made above, resulting in the `Wait()` to continue on before this is executed
        for t := range queue {
            slice = append(slice, t)
            wg.Done()   // ** move the `Done()` call here
        }
    }()

    wg.Wait()

    // now prints off all 100 int values
    fmt.Println(slice)
}
11
chris

Un canal est le meilleur moyen de s'y attaquer. Voici un exemple qui peut être exécuté sur go playground .

package main

import "fmt"
import "sync"
import "runtime"

type T int

func main() {
    var slice []T
    var wg sync.WaitGroup

    queue := make(chan T, 1)

    // Create our data and send it into the queue.
    wg.Add(100)
    for i := 0; i < 100; i++ {
        go func(i int) {
            defer wg.Done()

            // Do stuff.
            runtime.Gosched()

            queue <- T(i)
        }(i)
    }

    // Poll the queue for data and append it to the slice.
    // Since this happens synchronously and in the same
    // goroutine/thread, this can be considered safe.
    go func() {
        defer wg.Done()
        for t := range queue {
            slice = append(slice, t)
        }
    }()

    // Wait for everything to finish.
    wg.Wait()

    fmt.Println(slice)
}

Note: l'appel runtime.Gosched() est présent car ces routines ne sont pas transmises au planificateur. Ce qui provoquerait un blocage si nous ne faisons pas explicitement quelque chose pour déclencher ledit planificateur. Une autre option aurait pu consister à effectuer des E/S (par exemple: imprimer sur une sortie standard). Mais je trouve que runtime.Gosched() est plus facile et plus clair dans son intention.

1
jimt