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.