Aller au contenu

La Concurrence (Go Tour 4/4)

La concurrence est au cœur du langage Go et c’est souvent ce qui motive les développeurs à utiliser Go.

Goroutines

Une goroutine est une coroutine (ou un thread) performante, gérée directement par Go (et non par le système d’exploitation). Pour démarrer une goroutine avec une fonction, il suffit d’appeler cette fonction avec le mot clé go :

go f(x, y, z)

Les goroutines s’exécutent dans le même espace d’adressage, donc l’accès à la mémoire partagée doit être synchronisé. Le package sync fournit des primitives utiles telles que les sémaphores, mais vous verrez que vous n’en aurez pas vraiment besoin en Go. En effet, les channels que nous verrons dans la section suivante offrent souvent des méthodes de synchronisation plus élégantes.

Channels

Les channels sont des types qui permettent d’envoyer et de recevoir des valeurs. C’est un peu l’équivalent d’une message queue que l’on trouve dans beaucoup de systèmes d’exploitation.

Si ch est une variable de type chan, alors on peut envoyer une valeur v dans le canal avec l’instruction :

ch <- v

et on peut lire du canal avec :

v := <-ch

Tout comme les slices ou les maps, les channels doivent être créés avec l’instruction make avant de pouvoir servir. L’instruction suivante crée un canal ch pouvant contenir des entiers (int)

ch := make(chan int)

Par défaut, l’envoi ou la réception de valeur au travers d’un canal bloque jusqu’à ce que l’autre côté soit prêt. Cela permet de synchroniser les goroutines sans nécessiter de mutex ou de sémaphore.

L’exemple suivant calcule la somme d’un slice en partageant le travail dans deux goroutines. Sur un système multicœur, chaque goroutine utilisera un cœur et si le slice est très grand, le résultat sera calculé plus rapidement.

func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s {
        sum += v
    }
    c <- sum // send sum to c
}

func main() {
    s := []int{7, 2, 8, -9, 4, 0}

    c := make(chan int)
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    x, y := <-c, <-c // receive from c

    fmt.Println(x, y, x+y)
}

Buffered channels

Les canaux peuvent aussi être associés à des tampons (buffers). Pour définir un canal avec un buffer, on ajoute un argument à l’instruction make. L’instruction suivante crée un canal permettant d’envoyer et de recevoir des entiers (int) avec un buffer de 100 éléments :

ch := make(chan int, 100)

Ce canal ne bloquera que si l’on essaye d’envoyer un entier alors que le buffer est plein, ou alors si l’on essaye de lire une valeur et que le buffer est vide.

Range et Close

Lorsqu’un producteur ne souhaite plus envoyer de données, il peut fermer un canal avec l’instruction close(ch). Un consommateur peut tester si le canal est encore ouvert avec une deuxième variable à l’opérateur <- :

v, ok := <-ch

La variable ok sera false dès que le canal sera fermé et qu’il ne pourra donc plus recevoir de données.

On peut aussi itérer sur un canal jusqu’à ce que ce dernier soit fermé avec l’instruction range :

for i := range ch

Notes

Seul le producteur doit fermer un canal, jamais le consommateur. Envoyer des données sur un canal fermé provoque une erreur au run-time.

Les cannaux ne sont pas comme des fichiers et en général, ce n’est pas nécessaire de les fermer. Il ne faut les fermer que si on souhaite informer le consommateur qu’il n’y a plus de données et ainsi permettre de terminer la boucle utilisant l’instruction range.

Select

L’instruction select/case est comparable à un switch/case, mais pour les canaux. Le code suivant implémente un producteur de nombres de Fibonacci. La goroutine s’arrête au select et peut faire trois choses :

  • Si elle peut envoyer des nombres sur le canal c, elle envoie le prochain nombre de la suite.
  • Sinon, si elle reçoit quelque chose sur le canal quit, la gouroutine termine.
  • Sinon, elle se met en pause et attend un des deux événements précités.
func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c <- x:
            x, y = y, x+y
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}

Au lieu d’attendre, on peut spécifier une autre action si aucun canal n’est utilisable avec le mot clé default

func main() {
    tick := time.Tick(100 * time.Millisecond)
    boom := time.After(500 * time.Millisecond)
    for {
        select {
        case <-tick:
            fmt.Println("tick.")
        case <-boom:
            fmt.Println("BOOM!")
            return
        default:
            fmt.Println("    .")
            time.Sleep(50 * time.Millisecond)
        }
    }
}

Sync.Mutex

Les canaux sont parfaits pour que des goroutines puissent communiquer entre elles. Mais si nous ne souhaitons que protéger un accès à une ressource partagée avec une exclusion mutuelle, nous pouvons utiliser un mutex.

Le package standard sync offre le type Mutex qui implémente les méthodes Lock et Unlock.

Une pratique souvent observée dans les programmes qui utilisent des mutex consiste différer l’appeler la méthode Unlock avec le mot clé defer.